さびた証拠

誰もがプログラミング言語で何か違うものを探しています。ある人にとっては機能的な側面が重要であり、ある人にとってはライブラリが豊富であり、他の人はキーワードの長さにすぐに注意を払うでしょう。 この記事では、Rustで私にとって特に重要なこと、つまりその自明性についてお話したいと思います。 最初から学ぶのは簡単ではありませんが、その断片を見ることでコードの振る舞いを調べるのがどれほど簡単かはわかりません。 特定の関数が何をするのかをコンテキストから除外して正確に判断できる言語の機能をリストします。



特に、私は言語を完全に説明しようとしているのではなく、言語の片側だけを説明していることに注意してください。 これはRustの哲学に対する私の個人的な見解であり、必ずしも開発者の公式の立場と一致しているわけではありません! さらに、Rustは他の言語の外国人には明らかではありません。学習曲線は非常に鋭く、コンパイラーはあなたを悟りへの道のりで何度も「wtf」と言わせます。



危険なコード-安全でない



「通常の」コードは、メモリアクセスに対して安全であると見なされます。 これはまだ正式には証明されていませんが、これは開発者の考えです。 このセキュリティは、コードが落ちないという意味ではありません。 これは、他の誰かの記憶や他の多くの不明確な振る舞いの読み取りがないことを意味します。 私の知る限り、通常のコードではすべての動作が定義されています。 ビルド中に追跡できない違法なことをしようとすると、起こりうる最悪の事態は制御されたクラッシュです。



言語の単純なルールを超えて何かを行う場合、対応するトリッキーなコードを安全ない{}でフレーム化します。 たとえば、同期プリミティブとスマートカウンター( Arc、Rc、Mutex、RwLock )の実装で安全でないコードを見つけることができます。 これらは完全に安全な(Rustの観点から)インターフェイスを公開するため、これらの要素は危険になりません。



//       GL   , //          fn clear(&self) { unsafe { self.gl.clear(gl::COLOR_BUFFER_BIT) } }
      
      





そのため、安全でないブロックを持つ関数に出くわした場合は、内容を注意深く見る必要があります。 そうでない場合、落ち着いて、関数の動作は厳密に定義されています( 未定義の動作はありません)。 この機会に...こんにちは、C ++!



そうでない例外



Javaコードがあります。他の関数を呼び出す関数などです。 また、必要に応じてツリーを構築するために、誰がどこに戻ってくるのかを追跡できます。 しかし問題は、各関数呼び出しが通常の方法で、または例外を介して戻ることができることです。 どこかで捕まって処理され、どこかで二階に投げられます。 間違いなく、例外のシステムは、永久に使用できる強力で表現力豊かなツールです。 しかし、それは自明性を妨げます。コードの一部を見て、プログラマはどの関数がどの例外を引き起こし、それに対して何をすべきかを理解し、追跡しなければなりません。



そのため、ZeroMQの作成者は、この複雑さだけが干渉することを決定し、Rust開発者は彼に同意します。 例外はなく、潜在的なエラーは返される(代数)型の一部です。



 fn foo() -> Result<Something, SomeError>; ... match foo() { Ok(t) => (...), //! Err(e) => (...), //! }
      
      





どのような関数がどのように返されるかを確認し、潜在的なエラーをチェックせずに結果を取得することはできません(こんにちは、Go!)。



限定型推論



Pythonの関数を見ると、その機能が常に理解できるとは限りません。 もちろん、良い名前とパラメーター名がありますが、これは検証も保証もされていません。 このプロパティにより、言語をコンパクトにし、データではなくアクションに集中できます。



 # , , ,      def save_mesh(out, ob, log): # -> (Mesh, bounds, faces_per_mat): ... # 50       
      
      





Rustには型の推論がありますが、純粋にローカルです。 関数の戻り値の型、そのすべてのパラメーター、および静的な値の型を指定する必要があります。 このアプローチを使用すると、同じタイプが検索されている場合、別の関数のコードを探し回る必要がありません。



ローカル変数



このプロパティは非常にシンプルで明らかなようです... Oberonからの男が登場するまで(こんにちは!) グローバル変数にはプラスの側面がありますが、コードの断片を理解するのが難しくなります。



 // entity -  ,      for entity in scene.entities { //    -       entity.draw() }
      
      







不変変数



そうそう、デフォルトの変数を2回変更することはできません。 初期化後に変更する場合は、単語mutを指定してください。 この場合、コンパイラはこのプロパティを保証します。



 fn foo(x: &u32) -> u32 { ... //  ,    ,    *x + 1 }
      
      





または可変状態の場合:



 fn foo(x: &mut u32) { ... //    ,   ,   , //          ,    *x = *x + 1; }
      
      





これをCのconstと比較します。



 unsigned foo(const unsigned *const x) { ... //    ,         //         ,       return *x + 1; }
      
      







表示されないポインター



Rustにはポインターがありますが、危険なコードの小さな断片で厳密に使用されます。 この理由は、ポインターの参照解除は安全でない操作だからです。 カスタムコードはリンク上に豊富に構築されています。 オブジェクトへの参照は、生きている間その存在を保証します。 したがって、Rustでは、nullポインターの問題はありません。



もちろん、nullポインターは存在しないオブジェクトの意味を単に伝えるという大きな感嘆の声が聞こえます。これは、Rustでも同様に、後続のすべての論理エラーで表されます。 はい、 Option <&Something>がありますが、これはまったく同じではありません。 Rustの観点から見ると、Javaなどのコードには、アクセス時にクラッシュする可能性のあるポインターがたくさんあります。 ゼロになれないものは知っているかもしれませんが、それを覚えておいてください。 あなたの同僚はあなたの心を読むことができず、コンパイラはメモリ障害からあなたを保護することができません。



Rustでは、欠落しているオブジェクトのセマンティクスは明白です。コード内で明示的であり、コンパイラーは、ユーザー(および同僚)にアクセス時にオブジェクトの存在をチェックするよう義務付けています。 私たちが扱うほとんどのオブジェクトは、単純なリンクを介して送信され、その存在は保証されています:



 fn get_count(input: Option<&str>) -> usize { match input { Some(s) => s.len(), None => 0, } }
      
      





もちろん、あなたはまだそこにない何かを期待する場所に落ちることができます。 しかし、この秋は意図的に( unwrap()またはexpect()呼び出しを介して)明示的です。



モジュール



スコープ内のすべては、ローカル広告とキーワードを使用して見つけることができます。 コードのブロックでスコープを直接拡張できます。これにより、ローカリティがさらに強化されます。



 fn myswap(x: &mut i8, y: &mut i8) { use std::mem::swap; swap(x, y); }
      
      





問題は本質的にCとC ++のみにありますが、そこにはかなりの成果があります。 視界の分野で何を正確に理解するには? 現在のファイルをチェックしてから、含まれているすべてのファイルをチェックしてから、含まれているすべてのファイルをチェックする必要があります。



継承ではなく構成



Rustにはクラス継承がありません。 インターフェイス(特性)を継承できますが、構造体は、必要なインターフェイスを継承したすべてのインターフェイスを明示的に実装する必要があります。 object.foo()メソッドの呼び出しを見たと仮定します。 どのコードが実行されますか? 継承(特に複数)を使用する言語では、実装を見つけるまで、 オブジェクトの型のクラス、その親クラスなどでこのメソッドを探す必要があります。



継承は、膨大な数のタスクで美しい多態性を実現できる強力な武器です。 Rustでは、言語の美しさを保ちながら、類似の方法を得る方法についてはまだ議論がありません 。 しかし、それによってコードが理解しにくくなると確信しています。



継承がなければ、状況は少し平準化されます。 最初に、構造自体の実装を確認します。メソッドが存在する場合、ストーリーはそこで終了します。 次に、どのインターフェイスがスコープ内にあり、どのインターフェイスがこのメソッドを持ち、どのインターフェイスが構造に実装されているかを確認します。 これらのサブセットの交点には、コンパイラーが誓わない場合、単一のインターフェースがあります。 コード自体は、構造体によるこのインターフェースの実装、またはその宣言自体(デフォルト実装)に配置されます。



汎化の明示的な実装



それとは別に、特定のインターフェイスを満たすためには、明示的に指定する必要があるという点に注意してください。



 impl SomeTrait for MyStruct {...}
      
      





これは、インターフェイスまたはターゲット構造が宣言されている場所で実行できますが、任意の場所ではできません。 こんにちはGo、暗黙の現実の魔法が君臨します。 いいえ、Goのコンセプトは非常に美しく独創的です。私は主張しませんが、何が起こっているのかという証拠を疑います。



一般的な制限



C ++のパターンは、奇妙なことに、メタプログラミング要素です。 チューリングでいっぱいの一種の成熟したマクロ。 これらを使用すると、多くのコードを保存し、実際の奇跡(hello、Boost!)を実行できます。 ただし、特定のタイプの置換の瞬間に正確に何が起こるかを言うのは困難です。 置換可能なタイプの要件もすぐには明らかになりません。



Rust(および他の多くの言語)には、テンプレートの代わりに一般化があります。 それらのパラメーターは、置換用の特定のインターフェースを提供するために必要であり、そのような一般化の正確性は、コンパイラーによって確実に検証されます。



 //         pub fn max<T: Ord>(v1: T, v2: T) -> T
      
      





委員会がこの概念の重要性を認識していることは注目に値するので、C ++でも似たようなものをすぐに見ることができます。



非特殊な一般化



C ++には、テンプレートの特化という素晴らしい点があります。 これにより、特定のタイプの共通機能の動作を柔軟に再定義できます。 実行速度を向上させるか、コードの量を減らすことができますが、この機能には代価があります。 メソッドのテンプレートと実際の動作を見ると、これらの特殊化のためにコードベース全体を調べたときにのみ知ることができます。 Rustの方が簡単です。前に一般化されたメソッドがある場合、そのコードを他の場所で検索する必要はありません。



言語の自明性は局所性に由来します 。コードの動作を決定するすべてのものは、そのすぐ近くにあります。 これらのプロパティは予測可能性をもたらします。各機能のテストを記述し、デバッガで実行してその機能を理解する必要はありません。 ただし、予測可能性により、他の人のコードを読みやすくし、自分でエラーを発見(または防止)することが容易になります。これにより、 制御が向上ます。 プログラマーにとって、これはすべて、チーム開発とデバッグの容易さ、将来への自信、そして良い夢です。



錆は暗黒魔術ではありません。死者を蘇らせず、水をワインに変えません。 同様に、それは私たちの工芸のすべての問題を解決するわけではありません。 ただし、潜在的な問題が表面化するような方法でコードを考えて記述します。 ある意味で、Rustはプログラミングの現実を歪め、ワープドライブのように動き回ることを容易にします。



All Articles