論理的だが違法

C ++の栄光の世界から.NETの栄光の世界に来た多くの人は、言語がこのように振る舞うのであり、そうではない理由を理解するために文字通り標準を掘り下げなければならなかった方法を非常によく覚えていると思います。 綿密に調べてみると、彼らにとって完全に明らかである多くのことは、それほど明白ではないことが判明しました-しかし、私たち全員が頼っていた常識の正反対です。



ただし、ほとんどの場合、これは多くのプログラミング言語の問題です。 JavaScript言語とRuby言語の「自明性」の問題に特化した有名なWAT映画を覚えている人は多いと思います。 おなじみの世界の論理は、境界領域が現れると煙になります-普通の人が登らない領域です。



ただし、これらの重要事項から少し逃れることをお勧めし、C#言語を少し異なる、異常な側面から見てみましょう。 すなわち、一方では完全に理解可能であり、言語の観点から簡単に説明され、他方ではコンパイルを完全に拒否するいくつかの構造を見るために。



コンパイルして動作する必要があるかどうかについても議論するつもりはありません。 私たちにとって「論理的で理解可能な」ものはすべて、実際には完全に非論理的で理解不能なものになりかねないことを思い出します。



そして、はい-かさばる構造を使用せずに関数tuplから何を返したいのか、単純な演算子を使用して変数名を取得したいというトピックについて、もう一度泣き言を言うことはありません。 これはここにありません-これはMicrosoftに対する機能リクエストです。 5分間のユーモアがあります。 だから、あなたが毎日書きたいが、あなたが知っているので、書かないでください-それはコンパイルされません。 行こう!



最も単純なものから始めましょう。



int i = 0;
      
      







何が聞こえますか 「このコードは何をしますか?」 職業から抜け出す! このような知的多数派は、私たちのエッグヘッドのクラブには居場所がありません。 このコードは何もしません-コンパイラーは最適化中にそれを捨て、完全に正しいでしょう。 しかし、このレコードの意味は精神薄弱者にも理解できます。名前iの変数を定義し、0に等しい値を割り当てます。



さらに複雑な例に移りましょう。



 int i = 0; int j = i;
      
      







部屋のもう半分は私たちの集会を去り、例の途方もない複雑さを理解することができませんでした。 タブを閉じないでください-クライマックスの前に大気を演劇的に膨らませています。



これを試してみましょう

 int i = 0, j = 0;
      
      





そしてそう

 int i = 0, j = i;
      
      





問題ありません。



しかし、計算を行いたいと思うとすぐに、コンパイラーは存在しない状態からすぐに現れ、メーターの定規を手に入れます。 彼らは、あえてコードに方程式を散らかすことはしないでください!



 int i = j = 0; // error CS0103: The name `j' does not exist in the current context
      
      







しかし、ロシア人はあきらめません!



 int i = int j = 0; // error CS1525: Unexpected symbol `j', expecting `.'
      
      







精神の弱い人が私たちのスキルフルハンドサークルを離れたので、三項演算子との議論をいくらか多様化することができます!



 int i = whatever ? 1 : 2;
      
      







毎日このようなコードを書いています。 しかし、誰もが知っているわけではありません(特に、C ++で記述したことがない人は、この言語のセマンティクスをC#よりも数倍強力であり、多くの操作が同じ方法で行われていることに気付かない人のように)、このようにすることができます。



 int i = whatever ? (j = 1) : 2;
      
      







このエントリが謎である人々があることを疑います。 それは単純なことを意味します-もしそうであれば、jを1に割り当て、割り当ての結果(つまりjの値)を変数iに割り当てます。 そうでなければ、彼女に2を割り当てます。この信じられないほどの力で武装し、腰から撃ちます!



 int i = whatever ? 2 * (j = 1) - 1 : 2;
      
      







しかし、人は常に奇妙な何かを望んでいます-例えば、三項演算子で双三次方程式を解くために。 または、単に何かを残します。



 int i = whatever ? { j = 1; return 2; } : 2; // error CS8025: Parsing error
      
      







悲しいかな、人間の心は血に飢えた機械に直面して失敗しています。 彼は回避策を見つけようとしています...



 int i = whatever ? ( () => 1 )() : 2; // error CS0119: Expression denotes a `anonymous method', where a `method group' was expected
      
      







だから私はコンパイラが叫ぶのを聞きます:

-はい、私はあなたを代理人にします、私はあなたがちょうどどのタイプを教えてくれますか?

-誰でも!

-何?

-誰でも。 誰でも。 リュウユユボゴ!

-System.MulticastDelegate。

-いいえ、まあ、もちろんそうではありません...



(ところで、誰がYouTubeでこのビデオを見つけるのを手伝ってくれる-私は非常に感謝します)



そして彼を見つけました!



 int i = whatever ? new System.Func<int>( () => 1 )() : 2;
      
      







残念ながら、彼は元のアイデアほど美しくはありません。



 int i = whatever ? new System.Func<int>( () => { j = 1; return 2 + j; } )() : 2;
      
      







しかし、魚が不足しているため、彼らが言うように、ブーツはイワシです。



残りの半分はすでに大声でいびきをかいているので、複雑な指示-ブロックに進むことができます。



私たちは皆、サイクルを書きました。



 for(int i = 0; i < 10; i++);
      
      







実際、このエントリは次のようになります。



 { int i = 0; while(i < 10) i++; }
      
      







まあ、そのような



 int i; for(i = 0; i < 10; i++)
      
      







このコードに似ています。



 int i = 0; while(i < 10) i++;
      
      







一般に、このための特別なブロックを割り当てるのではなく、命令自体で直接制御命令で変数を宣言するのは素晴らしいことです。 そうでなければ、多くのコンパクトなものをはるかに複雑な方法で記述する必要があります。 これは特にusingブロックに当てはまります。



 using(IDisposable data = GetSomeData()) { }
      
      







それはそのようなものに展開します



 { IDisposable data = null; try { data = GetSomeData(); } finally { if(data != null) data.Dispose(); } }
      
      







ここで、「短縮形」で次の表現を書きたいと想像してください。



 { int i = 5; if(i < 10) i = 7; }
      
      







制御ステートメントに変数宣言を導入することは論理的です。 勝利



 if((int i = 5) < 10) // error CS1525: Unexpected symbol `i', expecting `)' or `.' i = 7;
      
      







とても近い...



わかりましたが、C#はオブジェクト指向です。 オブジェクトで遊ぼう!



派生クラスのコンストラクターから親コンストラクターを簡単かつ効果的に呼び出した方法を思い出してください。 なんとエレガントで理解しやすい構文がありましたか?



 class SomeClass { public SomeClass(int data) { } } class SomeOtherClass : SomeClass { public SomeOtherClass(int data) : base (data) { } }
      
      







そして、私はすぐに奇妙な何かが欲しいです。



 class SomeClass { public virtual void SomeMethod(int data) { } } class SomeOtherClass : SomeClass { public override void SomeMethod(int data) : base (data) { } // Unexpected symbol `:' in class, struct, or interface member declaration }
      
      







筆者が最後にコンパイラに私が書いた形式が理にかなっていることを納得させようとしたとき、それは私のof骨の2つを壊しました。 実験に注意してください!



そして今、たった一人の人が私を読んでいるとき(はい、あなただけです-離れないでください-私は孤独を恐れています)、私たちは別の例を見るでしょう。可能ですが、それでも私を困惑させるほど奇妙です。



ちなみに、他のバージョンとは異なり、すべてのバージョンでコンパイルされます。



毎日行うことはすべて行いますが、やや珍しい方法です。 いわゆるサブサブから。



1つのインターフェイスを実装する2つのクラスの階層があります。 そして、私たちは次の課題に取り組み、結果を観察します。



 new FooA().Foo(); new FooB().Foo(); ((IFoo) new FooA()).Foo(); ((IFoo) new FooB()).Foo(); ((FooA) new FooB()).Foo(); ((IFoo)(FooA) new FooB()).Foo();
      
      







私たちはとても面白いからです。



最も単純な仮想メソッドから始めましょう。 本のような。



 interface IFoo { void Foo(); } class FooA : IFoo { public virtual void Foo() { System.Console.WriteLine("A"); } } class FooB : FooA, IFoo { public override void Foo() { System.Console.WriteLine("B"); } }
      
      







結果は明らかです!



 new FooA().Foo(); // A new FooB().Foo(); // B ((IFoo) new FooA()).Foo(); // A ((IFoo) new FooB()).Foo(); // B ((FooA) new FooB()).Foo(); // B ((IFoo)(FooA) new FooB()).Foo(); // B
      
      







しかし、私たちは賢いです。 なぜ仮想メソッドが必要なのですか? インターフェースがあります。



 interface IFoo { void Foo(); } class FooA : IFoo { public void Foo() { System.Console.WriteLine("A"); } } class FooB : FooA, IFoo { public void Foo() { System.Console.WriteLine("B"); } }
      
      







もちろん、警告が表示されます



 // warning CS0108: `FooB.Foo()' hides inherited member `FooA.Foo()'. Use the new keyword if hiding was intended
      
      







しかし、注意を払わないでください-問題はその中にありません。新しいキーワードは実験にまったく影響しません。



結果は圧倒的です。



 new FooA().Foo(); // A new FooB().Foo(); // B ((IFoo) new FooA()).Foo(); // A ((IFoo) new FooB()).Foo(); // B ((FooA) new FooB()).Foo(); // A ((IFoo)(FooA) new FooB()).Foo(); // B
      
      







ここで最も恐ろしいことは、基本クラスへのキャストを介してメソッドを呼び出すことと、基本クラスにキャストしてからインターフェイスにメソッドを呼び出すことの違いです。 メソッドが仮想メソッドとして動作するのは、1つの場合を除き、基本クラスにキャストするためです。



インターフェイスなしでそれをエミュレートしてみましょう。 インターフェイスが常に階層の最初のクラスメソッドを呼び出すと仮定します。



 class IFoo { public void Foo() { System.Console.WriteLine("A (I)"); } } class FooA : IFoo { public void Foo() { System.Console.WriteLine("A"); } } class FooB : FooA { public void Foo() { System.Console.WriteLine("B"); } }
      
      







悲しいかな、大失敗-振る舞いは私たちが望むものとはまったく異なります。



 new FooA().Foo(); // A new FooB().Foo(); // B ((IFoo) new FooA()).Foo(); // A (I) ((IFoo) new FooB()).Foo(); // A (I) ((FooA) new FooB()).Foo(); // A ((IFoo)(FooA) new FooB()).Foo(); // A (I)
      
      







彼が階層の最後のメソッドを呼び出した場合、その動作もインターフェイスでの動作とは大きく異なります。



そのような動作を理解できるロジックはありません。 改訂4の2005年基準第2巻第3部第7章7段落1のパラグラフ3のパラグラフ14が、第4月の第3週の第2回会議の第1回読会で採択されたことを除きます。



残念ながら、私は自己反省の理由について考えておく必要があります-残念ながら、私のコンパイラは金曜日をあまりにも早く祝い、彼らは今、標準でキッチンでいびきをかいています。 おそらく私も同じことをするでしょう。 素敵な夢と良い週末を!



All Articles