数週間前、Andrei AlexandrescuによるRust、D and Goの比較分析に出会いました。 C ++コミュニティの尊敬されているメンバーで あり、Dプログラミング言語の主任開発者である Andreiは、ストーリーの最後でRustに衝撃的な打撃を与え、かなり洞察力に富んだ観察のように見える何かを述べました。
Rustコードを読むと、「友人は友人に足の1日を逃してはいけない」というジョークを思い起こさせ、andせた脚でバランスの取れた胴体の男性の漫画画像を連想させます。 Rustは、安全性とジュエリーの取り扱いを最重要視しています。 実際、これはめったに本当の問題ではなく、このアプローチはコードを考えて書くプロセスを単調で退屈なプロセスにします。
アンドレイとのいくつかの会議の後、彼のスピーチのいくつかを見て、私は彼がからかうのが好きだと確信しました。 しかし、餌を飲み込みましょう。 このジョークはおもしろそうに見えるという理由だけで面白いのでしょうか、それともジョークの一部しか持っていないからでしょうか?
ブラバの逆説
毎回、プログラミング言語の特定の機能の利点を反映して、Paul Graham のエッセイ「征服する平凡さ」に戻ります。 プログラマーの間の興味深い現象について語っており、彼はこれを「パラドックスオブブラバ」と呼んでいます。 知らない人にとっては、逆説は次のように聞こえます。ある言語のBlabを使用するプログラマがあるとしましょう。 表現力の面では、Blabはすべてのプログラミング言語の抽象性の連続の真ん中にあります。 これは最も原始的ではありませんが、最も強力なプログラミング言語ではありません。
私たちのBlubプログラマーは、プログラミング言語の「下位」部分を見ると、これらの言語が自分の愛するBlubよりも表現力に乏しいことを簡単に観察します。 しかし、私たちの仮想プログラマーがスペクトルの「上位」部分を見るとき、彼は通常彼が実際に見上げていることに気づきません。 ポールは次のように説明しています。
彼が見ているのは「奇妙な」言語だけです。 おそらく、彼はそれらをBlabaと同等であると認識していますが、それらの中にはまだ無意味で理解できないゴミがたくさんあります。 プログラマーにとっては十分です。彼自身がBlabaについて考えているからです。
私がこれを最初に読んだとき、私は思いました:「うわー、これは非常に洞察力に富んでいます。」 何年後にこの概念が私の考え方に根付くと誰が考えたでしょう。私が人々にプログラミングの方法を教えようと試みたとき。
Microsoftの言語のプロジェクトマネージャーとして、Javascriptの型付きバージョンであるTypeScriptに取り組んでいます。 間違いなく、主にJavaScript開発者の聴衆と話をし、Javascriptに少し強い型付けを追加するのがどれほど素晴らしいかという考えを伝えようとすると、悲観的な顔が私を見ます。 毎回。 オプションであっても。 半ダースのメリットを説明した後でも。 ポールが言ったように、それはただ「奇妙」に見えます。 JavaScriptプログラマーにとって、TypeScriptは基本的にJavaScriptと同じように見えますが、バカげたものやあいまいなものがたくさんあります。
他のプログラミング言語のチームと話をしたり、会議でますます多くの人を見ると、ポールの観察は正確であるだけでなく、驚くほど普遍的であることがわかりました。 ほとんどのプログラマーは、一度も使用したことのない新しいプログラミング言語を目にすると、あらゆる力で抵抗する準備ができています。 新しい、それらに異質な機能は、それらにアレルギー反応を引き起こします。 長い間新機能を使用して初めて、彼らはこれがすべて無駄な迷走ではないことを理解し始めます。
要するに、Blobのパラドックスは、プログラマーとして私たちが考慮しなければならないもの、私たちが陥りがちなもの、そして私たちがすべての努力で脱出すべきものです。
やってみましょう。 Rustの最も奇妙で最も役に立たない機能をいくつか見てみましょう。 そして、非ブロック化を実行できるかどうかを確認します。
奇妙なでたらめ番号1。 さび多型
Rustで、2つの異なる構造を印刷するために少しのポリモーフィズムを使用するプログラムを作成しましょう。 最初にコードを示し、次に詳細に調べます。
use std::fmt; struct Foo { x: i32 } impl fmt::Display for Foo { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "(x: {})", self.x) } } struct Bar { x: i32, y: i32 } impl fmt::Display for Bar { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "(x: {}, y: {})", self.x, self.y) } } fn print_me<T: fmt::Display>(obj : T) { println!("Value: {}", obj); } fn main() { let foo = Foo {x: 7}; let bar = Bar {x: 5, y: 10}; print_me(foo); print_me(bar); }
それはなんとvyrviglaznoeです! はい、ポリモーフィズムがありますが、これはOOPによく似ていません。 このコードでは、一般化だけでなく一般化も使用しますが、このアプローチには多くの制限があります。 そして、このimpl
は何ですか?
部分に行きましょう。 値を保存するために2つの構造を作成します。 それらに実装する次のステップは、 fmt::Display
と呼ばれるものです。 C ++では、 ostream
<<
演算子をオーバーロードします。 結果は同様です。 これで、print関数を呼び出して、構造体を直接渡すことができます。
これはすでに話の半分です。
次に、 print_me
関数を取得します。 この関数は一般化されており、 fmt::Display
できる場合は何でも使用できます。 幸いなことに、私たちは構造がそれを行えることを確認しました。
他のすべては簡単です。 構造体のいくつかのインスタンスを作成し、それらをprint_me
印刷するために送信します。
ふう...一生懸命働かなければなりませんでした。 これが、Rustでポリモーフィズムが行われる方法です。 全体のポイントは一般化です。
では、C ++に少し切り替えましょう。 多くの、特に初心者は、テンプレートの使用をすぐに考えることができず、オブジェクト指向のポリモーフィズムの道をたどります。
#include <iostream> class Foo { public: int x; virtual void print(); }; class Bar: public Foo { public: int y; virtual void print(); }; void Foo::print() { std::cout << "x: " << this->x << '\n'; } void Bar::print() { std::cout << "x: " << this->x << " y: " << this->y << '\n'; } void print(Foo foo) { foo.print(); } void print2(Foo &foo) { foo.print(); } void print3(Foo *foo) { foo->print(); } int main() { Bar bar; bar.x = 5; bar.y = 10; print(bar); print2(bar); print3(&bar); }
かなり簡単ですね。 さて、ここに小さなクイズがあります:C ++コードは正確に何を印刷しますか?
推測していなければ、落胆しないでください。 あなたは良い仲間です。
ご想像のとおり-おめでとうございます! ここで、正しい答えを出すためにC ++についてどれだけ知っておくべきかを少し考えてください。 私が見るものから、スタックがどのように機能するか、オブジェクトがコピーされるときにどのようにコピーされるか、ポインターがどのように機能するか、リンクがどのように機能するか、仮想テーブルがどのように配置されるか、そして動的ディスパッチとは何かを理解する必要があります。 OOPスタイルでいくつかの簡単な行を書くだけです。
私がC ++を学び始めたとき、この登山は私にとってあまりにも急であることが判明しました。 幸いなことに、私のいとこはC ++の専門家であることが判明し、私を彼の翼の下に連れて行って、彼は私に打たれた道を見せてくれました。 それにもかかわらず、私はこの例のように、なんとか子どもたちの過ちを犯しました。 なんで? C ++の難攻不落の理由の1つは、その開発における高い認知負荷です。
認知的負荷の一部は、本質的にプログラミングに固有のものに当てはまります。 スタックを理解する必要があります。 ポインターがどのように機能するかを知る必要があります。 しかし、C ++は負荷の度合いを増加させるため、値が完全にコピーされない場合、仮想ディスパッチが使用される場合、および使用されない場合を理解する必要があります-開発者が何かを行う場合、コンパイラからの警告なしで、 「ほとんどの場合、悪い考えです。」
これは、C ++との戦争を試みるものではありません。 Rustの多くのものは、C ++から取られた低レベルで効率的な抽象化の哲学を維持するという考え方で実装されています。 Rustの例と非常によく似たコードを書くこともできます。
Rustが実際に行うのは、継承をポリモーフィズムとは別にすることです。これにより、最初から一般化を作成する方向に考えさせることができます。 したがって、あなたは一般的な用語で最初の日から考え始めます。 ただし、継承を多態性から分離することは、特にそれらを常に一緒に使用することに慣れている場合は特に、奇妙なアイデアのように思えるかもしれません。
このような分離は、Blub効果の最初の兆候の1つを引き起こす可能性があります。継承とポリモーフィズムを分離する一般的な利点は何ですか? ところで、Rustには継承がありますか?
信じられないかもしれませんが、少なくともRust 1.6では、構造を継承するための特別なツールはまったくありません。 代わりに、言語の特別な概念である「タイプ」の助けを借りて、それらの機能が構造自体の境界を超えて拡張されています。 特性を使用すると、メソッドを追加したり、メソッドの実装を要求したり、既存のシステムのあらゆる方法でデータ構造を改良したりできます。 トレイトは継承もサポートします。あるトレイトは別のトレイトを拡張できます。
深く掘り下げると、何か他のものに気付くことができます。 Rustには、C ++で心配する必要のあった問題がすべてありません。 関数が何らかの方法で呼び出されたときに何かが失われることや、仮想ディスパッチがコードに与える影響について考えることはできなくなりました。 Rustでは、タイプに関係なく、すべてが同じスタイルで機能します。 したがって、幼年期の間違いの全体のクラスは単に消えます。
(レーンに注意してください。-タイプに関する詳細は、本「ロシア語プログラミング言語」のロシア語の翻訳にあります。)
奇妙なでたらめ数2。 つまり、例外はありませんか?
Rustにはないものについて話しているので、次の奇妙なことは例外がないことです。 それは一歩後退ではありませんか? 私たちは間違いをどうしますか? 一度にすべてを処理するためにそれらをプッシュできますか?
さて、モナドを知る時間です。
でも...大丈夫、冗談ですが、今回はそれらなしでもできます。 Rustでは、エラー処理ははるかに簡単です。 実際にどのように見えるかの例を次に示します。 まず、関数宣言がどのように見えるかの例:
impl SystemTime { /// pub fn now() -> SystemTime; /// , "" pub fn duration_from_earlier(&self, earlier: SystemTime) -> Result<Duration, SystemTimeError>; }
now
関数は単にSystemTime
を返すだけで例外はありませんが、 duration_from_earlier
はDuration
またはSystemTimeError
のいずれかの値を取ることができるRsult
型を返すことに注意してください。 したがって、関数のすべての可能な結果(成功と失敗の両方)がすぐにわかります。
しかし、これらの例外的な状況はすべて、戻り値に混乱をもたらします。 誰がコードでこれを見たいですか? もちろん、常にエラーをチェックするのは素晴らしいことですが、例外のポイントは、エラーをローカルで処理できるだけでなく、トップに転送して1か所で処理できることです。
Rustでも同じことができます。
fn load_header(file: &mut File) -> Result<Header, io::Error> { Ok(Header { header_block: try!(file.read_u32()) }) } fn load_metadata(file: &mut File) -> Result<Metadata, io::Error> { Ok(Metadata { metadata_block: try!(file.read_u32()) }) } fn load_audio(file: &mut File) -> Result<Audio, io::Error> { let header = try!(load_header(file)); let metadata = try!(load_metadata(file)); Ok(Audio { header: header, metadata: metadata }) }
完全には明らかではありませんが、このコードはスロー例外を使用します。 try!
マクロのチップ全体try!
。 彼はかなり簡単なことをしています。 関数を呼び出します。 成功した場合、彼は計算結果をあなたに渡します。 代わりにエラーが発生した場合は、 try!
このエラーがスローされ、現在の関数の実行が完了します。
つまり、 load_header
呼び出すときにfile.read_u32
に問題がある場合、関数はio::Error
を返します。 さらに、 load_audio
同じことが起こり、同じエラーが返されます。 呼び出し元の関数が最終的にエラーを処理するまで続きます。
(レーンに注意してください。- エラー処理の詳細については、Habrの記事「Rustでのエラー処理」を参照してください。)
奇妙なでたらめの数3。 借用チェッカー
これは面白いです。 Rustについて話すときに多くの人が最初に言及するのは、借入チェッカーです。 さらに、他のプログラミング言語と区別して、Rustの主な機能として提示されることがよくあります。 たとえば、Andreyの場合、ボローチェッカーは「ハルク型の胴体」Rustです。 私にとって、ボローチェッカーは単なるコンパイラーチェックです。 タイプチェックと同様に、ボローチェッカーを使用すると、実行時に発生する前にほとんどのバグをキャッチできます。 以上です。 もちろん、最初は怪物のような仕掛けのように思えるかもしれませんが、ここでのポイントは、Rustがいくつかの新しい不可解な型システムを学ぶことを強制するということではなく、それと連携する能力があなたのために新しい筋肉を構築するということですプログラマー。
それでは、ボローチェッカーはどのようなエラーをキャッチしますか?
メモリを解放した後のポインターの使用
そうそう、古典的な状況では、最初にメモリを解放してから再び使用します。 ほとんどの場合、これがまさに恐ろしい「ヌルポインタ例外」でプログラムがクラッシュする理由です。
解放後使用を回避するC ++の「グッドプラクティス」が多数あります。RAIIを使用する、生のポインターの代わりに参照またはスマートポインターを使用する、APIの所有権と関係を借用するなどです。 Andreiによれば、それはすべて「コードを考えて書くプロセスを単調で退屈なプロセスに変えてしまう」ということです。 よく訓練されたC ++プログラマーのチームは、単調で退屈な作業を行うことで、ほとんどの解放後使用ミスを回避できます。これは、すべての「グッドプラクティス」に従い、高度な資格を持つC ++専門家のみでチームをcheし、補充することはないためです。
無効なイテレーター
C ++で繰り返し処理したコンテナを変更しなければならず、これにより将来突然クラッシュすることがありますか? しなければなりませんでした。 コンテナに少なくとも1つの要素を追加または削除した場合は、コンテナを実装してイテレータを無効にする必要があります。
私はこのレーキを踏むことはあまりありませんが、それでも時々発生します。
データの競合状態
Rustでは、データは共有または可変です。 データを変更できる場合、複数のストリーム間でデータを共有できないため、2つのストリームで同時に変更を開始する方法はなく、競合状態が発生します。 データが共有されている場合、データは変更できないため、任意の数のストリームから好きなだけ読み取ることができます。
C ++または他の多くの優れた並列ライブラリを備えた言語の世界から来た場合、そのような制限は厳しすぎるかもしれません。 幸いなことに、これはストーリー全体ではありませんが、より複雑な抽象化を作成するための一連の単純なルールを提供する基盤です。 物語の残りの部分は現在書かれています。 Rustエコシステムでは 、 同時実行指向のライブラリが増えています。 もっと学ぶことに興味があるなら、彼らの仕事の原理を学ぶことができます。
所有権の追跡
この概念は少し冗長に見えるかもしれませんが、実際、これはまさにC ++が常に戦っているものです。 前に、「APIの所有権と借用関係を文書化する」ための優れたプラクティスの1つに言及しました。 問題は、この情報がコードに直接含まれるものではなく、コメントに保存されることです。
スクリプトは次のとおりです。C++で記述し、他の誰かが記述したライブラリを呼び出す必要があります。 これがCライブラリであり、引数として生のポインタを取るとしましょう。 このライブラリに転送したものを後で削除するように注意する必要がありますか? または、彼女は自分の構造の1つにデータを保存することでこの責任を負いますか? Rubyのようなスクリプトエンジンを呼び出しているのでしょうか? その後、誰がデータを所有しますか?
ドキュメントを読む代わりに、Rustではボローチェッカーを使用してライブラリAPIの正しい使用を常にチェックすることにより、期待を確実にすることができます。
その他
借用チェッカーは、他の多くのエラーを回避するのに役立ちます。 たとえば、作成した関数で受け入れる可変データは外部状態に影響を与えないという事実に常に依存することができ、適切と思われる場合は安全に変更できます。
ちなみに、これは、Cのようなプログラミング言語では困難な追加の最適化の幅広い機会を切り開きます。コンパイラーは、複数のエイリアスを持つ値は変更可能ではないことを保証しているため、その逆も同様です-変更可能な値には常に1つの名前しかありません。
(レーンに注意してください。所有権と借用の概念の詳細については、本「ロシア語プログラミング言語」のロシア語の翻訳を参照してください。)
でたらめな数4。 それらを破るにはルールが必要です
Rustの強みの1つはプラグマティズムだと思います。 厳密な制限のほとんどは、 unsafe
やmem::transmute
などの機能で回避できます。 ボローチェッカーはあなたの問題を解決するのに適していませんか? 問題ありません、ただ抜いてください。
(レーンに注意してください。-厳密に言えば、これは真実ではありません。Rustでは、借用チェッカーを無効にする簡単な方法はありません。安全でないブロック内でも、フルキャパシティで動作します。 unsafe
でunsafe
ブロックの場合と同様に、 生の *const T
および*mut T
ポインターを使用することもできます。これらは、Cからのポインターとほぼ同様に機能します。それらの使用は、借用規則によって制限されません。詳細については、 「The Rustonomicon:The Dark高度で安全でないRustプログラミングの技術 。 」
これにより、Cライクなシステムプログラミング言語で慣れていることなら何でもできます。 Rustの利点は、最初からデフォルトで安全なコードを記述し、必要に応じて安全でないセクションを追加する方がはるかに簡単であることです。 最初は安全でないものに基づいて安全なコードを書くことははるかに困難です。
Rustには選択肢がありますが、自分で足を撃たないように勧めています。
では、足の調子はどうですか?
彼の足元に戻って、Rustは彼のトレーニングを逃しましたか? 彼は一方的になりましたか? 彼は間違ったことに集中していましたか?
錆は毎日強くなっており、幸いなことに、彼は背中を曲げずにスクワットを行う方法をよく知っています。 この点を過大評価することは困難です。 Rustの哲学には強固な基盤があり、それは言語が成長し発展することを意味します。
翻訳者から:いつものように、私はロシア語を話すRustコミュニティの翻訳と貴重なコメントの助けに感謝しています。