なぜプログラミング言語が変数と関数のシャドウ/非表示を許可するのですか?


31

(などC ++やJava、Pythonなど)で最も人気のあるプログラミングlangugesの多くの概念持って隠しを / シャドーイング、変数や関数の。非表示またはシャドウイングに遭遇したとき、それらはバグを見つけるのが困難な原因であり、これらの言語の機能を使用する必要があることを発見したことはありません。

私には、非表示とシャドウイングを許可しない方が良いと思われます。

誰もがこれらの概念をうまく利用していることを知っていますか?

更新:
クラスメンバー(プライベート/保護されたメンバー)のカプセル化について言及していません。


これが、私のフィールド名がすべてFで始まる理由です。
Pieter B

7
Eric Lippertにはこれに関する素晴らしい記事があったと思います。ああ待って、ここにある:blogs.msdn.com/b/ericlippert/archive/2008/05/21/...
Lescai Ionel

1
質問を明確にしてください。一般的な情報隠蔽について、または派生クラスが基本クラスの機能を隠すLippertの記事で説明されている特定のケースについて尋ねていますか?
アーロンカーツハルス

重要な注意:非表示/シャドーイングによって引き起こされるバグの多くは、突然変異(間違った変数を設定し、なぜ変更が「決して起こらない」などと思っている)に関係しています。主に不変の参照で作業する場合、非表示/シャドウイングは問題をはるかに少なくし、バグを引き起こす可能性ははるかに低くなります。
ジャック

回答:


26

非表示とシャドウを許可しない場合、すべての変数がグローバルな言語になります。

グローバル変数または関数を隠す可能性のあるローカル変数または関数を許可するよりも明らかに悪いです。

もし不許可の隠蔽やシャドウイングた場合は、ANDあなたが特定のグローバル変数を「保護」しようとすると、あなたはコンパイラが、私は、デイブ申し訳ありませんが、しかし、あなたがその名前を使用することはできません」プログラマに伝え状況を作り出す、それがすでに使用されています」COBOLの経験から、この状況ではプログラマーはほとんどすぐに冒とくに訴えることがわかります。

基本的な問題は、非表示/シャドウイングではなく、グローバル変数です。


19
シャドウイングを禁止することのもう1つの欠点は、グローバル変数を追加すると、ローカルブロックで既に変数が使用されているためにコードが破損する可能性があることです。
ジョルジオ

19
「非表示とシャドウを許可しない場合、すべての変数がグローバルな言語になります。」-必ずしもではありません:シャドウイングなしでスコープ変数を使用できます。説明しました。
チアゴ・シウバ

@ThiagoSilvaは:そして、あなたの言語は、このモジュールがあることをコンパイラに指示する方法持っている必要がISのアクセスにそのモジュールの「frammis」変数を許可します。誰かが存在すら知らないオブジェクトを隠したり隠したりすることを許可したり、その名前を使用できない理由を伝えるために彼にそれを伝えたりしますか?
ジョンR.ストローム

9
@Phil、あなたに同意しないことを許しますが、OPは「変数または関数の隠蔽/隠蔽」について尋ね、「親」、「子」、「クラス」、および「メンバー」という言葉は彼の質問のどこにも現れません。それは名前の範囲についての一般的な質問のようです。
ジョンR.ストローム

3
@dylnmc、私は「2001:スペースオデッセイ」の参照が得られないほど十分に若いwhippersnapperに出会うほど長く生きるとは思っていませんでした。
ジョンR.ストローム

15

誰もがこれらの概念をうまく利用していることを知っていますか?

正確で記述的な識別子を使用することは常に有効です。

変数の非表示は多くのバグを引き起こさないと考えることができます。同じ/類似のタイプの2つの非常によく似た名前の変数(変数の非表示が許可されていない場合に行うこと)は、同じくらい多くのバグおよび/または重大なバグ。その引数が正しいかどうかはわかりませんが、少なくとも議論の余地はあります。

ある種のハンガリー記法を使用してフィールドとローカル変数を区別すると、これを回避できますが、メンテナンス(およびプログラマーの健全性)に独自の影響があります。

そして(おそらく、そもそもコンセプトが知られている理由である可能性が高い)、言語が隠蔽/シャドウイングを実装することは、それを禁止するよりもはるかに簡単です。実装が簡単になるということは、コンパイラにバグが発生する可能性が低くなることを意味します。実装が容易になるということは、コンパイラーが書くのにかかる時間が短くなり、プラットフォームがより早くより広く採用されることを意味します。


3
実際、いいえ、非表示とシャドウイングを実装するのは簡単ではありません。「すべての変数はグローバル」を実装する方が実際には簡単です。必要な名前空間は1つだけで、複数の名前空間を持ち、名前ごとにエクスポートするかどうかを決定するのではなく、常に名前をエクスポートします。
ジョンR.ストローム

5
@ JohnR.Strohm-もちろんですが何らかのスコープ(読み取り:クラス)があればすぐに、スコープで下位のスコープを非表示にすることは無料です。
テラスティン

スコープとクラスは異なるものです。BASICを除き、プログラミングしたすべての言語にはスコープがありますが、すべてのクラスにクラスまたはオブジェクトの概念があるわけではありません。
マイケルショー

@michaelshaw-もちろん、もっと明確にすべきでした。
テラスティン

7

同じページにいることを確認するために、メソッドの「非表示」は、派生クラスが基本クラスのメンバーと同じ名前のメンバーを定義するときです(メソッド/プロパティの場合、仮想/オーバーライド可能とマークされません) )、および「派生コンテキスト」の派生クラスのインスタンスから呼び出された場合、派生メンバーが使用されますが、その基本クラスのコンテキストで同じインスタンスによって呼び出された場合、基本クラスメンバーが使用されます。これは、基本クラスのメンバーが派生クラスが置換を定義することを期待するメンバーの抽象化/オーバーライド、および目的のスコープ外のコンシューマーからメンバーを「隠す」スコープ/可視性修飾子とは異なります。

許可されている理由に対する簡単な答えは、そうしないと、開発者がオブジェクト指向設計のいくつかの重要な教義に違反することを余儀なくされることです。

長い答えは次のとおりです。最初に、C#でメンバーの非表示が許可されていない代替ユニバースの次のクラス構造を検討します。

public interface IFoo
{
   string MyFooString {get;}
   int FooMethod();
}

public class Foo:IFoo
{
   public string MyFooString {get{return "Foo";}}
   public int FooMethod() {//incredibly useful code here};
}

public class Bar:Foo
{
   //public new string MyFooString {get{return "Bar";}}
}

Barのメンバーのコメントを解除し、そうすることで、Barが別のMyFooStringを提供できるようにします。ただし、メンバーの非表示に関する代替現実の禁止に違反するため、これを行うことはできません。この特定の例はバグが多いため、なぜそれを禁止したいのかという典型的な例です。たとえば、次の操作を実行した場合、どのコンソール出力が得られますか?

Bar myBar = new Bar();
Foo myFoo = myBar;
IFoo myIFoo = myFoo;

Console.WriteLine(myFoo.MyFooString);
Console.WriteLine(myBar.MyFooString);
Console.WriteLine(myIFoo.MyFooString);

私は頭のてっぺんから外れて、その最後の行に「Foo」と「Bar」のどちらが表示されるかわかりません。3つの変数すべてがまったく同じ状態のまったく同じインスタンスを参照している場合でも、最初の行に「Foo」、2番目の行に「Bar」が必ず表示されます。

そのため、言語のデザイナーは、私たちの別の世界では、プロパティの非表示を防ぐことにより、この明らかに悪いコードを思いとどまらせます。今、あなたはコーダーとして、まさにこれを行う必要があります。制限をどのように回避しますか?1つの方法は、Barのプロパティに異なる名前を付けることです。

public class Bar:Foo
{
   public string MyBarString {get{return "Bar";}}       
}

完全に合法ですが、私たちが望む行動ではありません。Barのインスタンスは、プロパティMyFooStringに対して "Foo"を常に生成します( "Bar"を生成したい場合)。IFooが特にバーであることを知る必要があるだけでなく、別のアクセサーを使用することも知っている必要があります。

また、親子関係を忘れて、インターフェースを直接実装することもできます。

public class Bar:IFoo
{
   public string MyFooString {get{return "Bar";}}
   public int FooMethod() {...}
}

この簡単な例では、FooとBarが両方ともIFooであることにだけ気をつけている限り、完璧な答えです。バーがFooではなく、そのように割り当てることができないため、いくつかの例の使用コードはコンパイルに失敗します。ただし、FooがBarに必要な便利なメソッド「FooMethod」を持っている場合、そのメソッドを継承することはできません。Barでコードを複製するか、クリエイティブにする必要があります。

public class Bar:IFoo
{
   public string MyFooString {get{return "Bar";}}
   private readonly theFoo = new Foo();

   public int FooMethod(){return theFoo.FooMethod();}
}

これは明らかなハックであり、OO言語仕様の一部の実装はこれより少ししか多くありませんが、概念的には間違っています。バーニーズの消費者はFooのの機能を公開する場合は、バーがすべきことではない、フー持って Fooのを。

もちろん、Fooを制御した場合、仮想化してからオーバーライドできます。これは、メンバーがオーバーライドされることが予想される現在のユニバースでの概念上のベストプラクティスであり、非表示を許可しない代替ユニバースを保持します。

public class Foo:IFoo
{
   public virtual string MyFooString {get{return "Foo";}}
   //...
}

public class Bar:Foo
{
   public override string MyFooString {get{return "Bar";}}
}

これに伴う問題は、仮想メンバーへのアクセスが内部で実行するのに比較的コストがかかるため、通常は必要なときにのみ実行することです。ただし、非表示がないため、ソースコードを制御していない別のコーダーが再実装したいメンバーについて悲観的になります。封印されていないクラスの「ベストプラクティス」は、特に望まない限り、すべてを仮想化することです。また、非表示の正確な動作も提供されません。インスタンスがバーの場合、文字列は常に「バー」になります。作業中の継承レベルに基づいて、隠された状態データのレイヤーを活用することが本当に役立つ場合があります。

要約すると、メンバーの非表示を許可することは、これらの悪の少ない方です。それがないと、一般的にオブジェクト指向の原則に対して、それを許可するよりもひどい残虐行為につながります。


実際の質問に対処するための+1。メンバーの非表示の実際の使用法の良い例は、トピックに関するEric Libbertのブログ投稿で説明されているIEnumerableand IEnumerable<T>インターフェースです。
フィル

オーバーライドは非表示ではありません。@Philがこの問題に対処していることに同意しません。
1月Hudec

私のポイントは、非表示がオプションではない場合、上書きが非表示の代わりになるということでした。私は同意します、それは隠れていません、そして私は非常に最初のパラグラフで同じように言います。C#で非表示にしないという私の代替現実のシナリオに対する回避策は非表示ではありません。それがポイントです。
キース

シャドーイング/非表示の使用が好きではありません。私が見ている主な良い使用法は、(1)新しいバージョンのベースクラスに、古いバージョンを中心に設計されたコンシューマコードと競合するメンバーが含まれている状況を回避することです(ugいが必要です)。(2)戻り型の共分散などの偽物。(3)基本クラスのメソッドが特定のサブタイプで呼び出し可能であるが役に立たない場合の処理。LSPは前者を必要としますが、ベースクラスコントラクトが特定のメソッドが特定の条件で役に立たない可能性があることを指定している場合は後者を必要としません。
supercat

2

正直なところ、C#コンパイラチームの主要な開発者であるEric Lippertがそれについて非常によく説明しています(リンクについてはLescai Ionelに感謝します)。.NET IEnumerableIEnumerable<T>インターフェイスは、メンバーの非表示が有用な場合の良い例です。

.NETの初期には、ジェネリックはありませんでした。そのため、IEnumerableインターフェースは次のようになりました。

public interface IEnumerable
{
    IEnumerator GetEnumerator();
}

このインターフェイスを使用してforeachオブジェクトのコレクションを上書きできましたが、それらを適切に使用するにはすべてのオブジェクトをキャストする必要がありました。

その後、ジェネリック医薬品が登場しました。ジェネリックを取得すると、新しいインターフェイスも取得しました。

public interface IEnumerable<T> : IEnumerable
{
    IEnumerator<T> GetEnumerator();
}

これで、オブジェクトを繰り返し処理している間にオブジェクトをキャストする必要がなくなりました!すごい!メンバーの非表示が許可されていない場合、インターフェースは次のようになります。

public interface IEnumerable<T> : IEnumerable
{
    IEnumerator<T> GetEnumeratorGeneric();
}

どちらの場合もまったく同じことを行うためGetEnumerator()、これはちょっとばかげていますが、戻り値がわずかに異なります。実際、これらは非常に似ているので、ジェネリックが.NETに導入される前に作成されたレガシーコードを使用している場合を除き、ほとんどの場合、常にジェネリック形式にデフォルト設定する必要があります。GetEnumeratorGeneric()GetEnumerator

時にはメンバーの隠蔽がない厄介なコードや困難に見つけるのバグのためのより多くの部屋を許可します。ただし、レガシーコードを壊さずに戻り値の型を変更する場合など、便利な場合があります。これは、言語設計者が下さなければならない決定の1つです。この機能を正当に必要とする開発者を不便にしますか、それともこの機能を言語に含めて、誤用の被害者からの軽視をしますか?


正式にはをIEnumerable<T>.GetEnumerator()非表示にしますがIEnumerable.GetEnumerator()、これは、オーバーライド時にC#に共変の戻り値型がないためです。論理的には、LSPに完全に沿ったオーバーライドです。非表示は、(C ++で)mapファイル内の関数にローカル変数がある場合ですusing namespace std
ジャン・ヒューデック

2

あなたの質問は2つの方法で読むことができます:変数/関数スコープ全般について尋ねているか、継承階層のスコープについてより具体的な質問をしています。継承については特に言及しませんでしたが、見つけにくいバグについては言及しました。これは単純なスコープよりも継承のコンテキストのスコープのように聞こえるので、両方の質問に答えます。

一般的にスコープは、プログラムの特定の(願わくは小さな)部分に注意を集中させることができるため、良いアイデアです。ローカル名が常に勝つため、特定のスコープ内にあるプログラムの部分のみを読み取れば、ローカルで定義された部分と他の場所で定義された部分を正確に把握できます。名前がローカルなものを参照している場合、その場合、それを定義するコードは目の前にあるか、ローカルスコープ外の何かへの参照です。(どこからでも変更することができ、特にグローバル変数、)私たちの下から出て変えることができる任意の非ローカル参照が存在しない場合、我々はローカルスコープ内のプログラムの一部が正しいかどうかを評価することができます参照せずプログラムの残りの部分にまったく

たまにいくつかのバグが発生する可能性がありますが、それ以外の場合に発生する可能性のある大量のバグを防止することで、それを補う以上のことができます。ライブラリ関数と同じ名前のローカル定義を作成する以外は(そうしないでください)、ローカルスコープでバグを導入する簡単な方法はわかりませんが、ローカルスコープは同じプログラムの多くの部分で使用できるものですiを互いに壊すことなくループのインデックスカウンターとして使用し、フレッドにホールと同じ名前の文字列を壊さないstrという名前の文字列を使用する関数を記述させます。

Bertrand Meyerによる、継承のコンテキストでのオーバーロードについての興味深い記事を見つけまし。彼は、構文のオーバーロード(同じ名前の2つの異なるものがあることを意味する)とセマンティックのオーバーロード(同じ抽象概念の2つの異なる実装があることを意味する)の間で興味深い区別をもたらします。セマンティックなオーバーロードは、サブクラスで異なる方法で実装するつもりだったので、問題ありません。構文のオーバーロードは、バグの原因となる偶発的な名前の衝突です。

意図されたバグとバグである継承状況でのオーバーロードの違いはセマンティクス(意味)であるため、コンパイラは、ユーザーが行ったことが正しいか間違っているかを知る方法がありません。単純なスコープの状況では、正しい答えは常にローカルなものであるため、コンパイラーは正しいものを把握できます。

Bertrand Meyerの提案は、Eiffelのような言語を使用することです。Eiffelは、このような名前の衝突を許可せず、プログラマーに一方または両方の名前変更を強制し、問題を完全に回避します。私の提案は、継承を完全に使用することを避け、問題を完全に回避することです。これらのいずれかを実行できない、または実行したくない場合でも、継承に問題がある可能性を減らすためにできることがあります:LSP(Liskov Substitution Principle)に従い、継承よりも合成を優先し、保持します継承階層を浅くし、継承階層のクラスを小さく保ちます。また、一部の言語では、たとえエッフェルのような言語のようにエラーを発行しなくても、警告を発行できる場合があります。


2

これが私の2セントです。

プログラムは、自己完結したプログラムロジックの単位であるブロック(関数、プロシージャ)に構造化できます。各ブロックは、名前/識別子を使用して「もの」(変数、関数、プロシージャ)を参照できます。名前からものへのこのマッピングは、バインディングと呼ばれます。

ブロックで使用される名前は、次の3つのカテゴリに分類されます。

  1. ブロック内でのみ知られている、ローカル変数などのローカルに定義された名前。
  2. ブロックが呼び出されたときに値にバインドされ、呼び出し元がブロックの入力/出力パラメーターを指定するために使用できる引数。
  3. ブロックが含まれる環境で定義され、ブロック内のスコープ内にある外部名/バインディング。

たとえば、次のCプログラムを考えます

#include<stdio.h>

void print_double_int(int n)
{
  int d = n * 2;

  printf("%d\n", d);
}

int main(int argc, char *argv[])
{
  print_double_int(4);
}

この関数print_double_intにはローカル名(ローカル変数)dと引数があり、スコープ内にあるがローカルに定義されていないn外部のグローバル名を使用しprintfます。

注意してくださいprintfまた、引数として渡すことができます。

#include<stdio.h>

void print_double_int(int n, int printf(const char *, ...))
{
  int d = n * 2;

  printf("%d\n", d);
}

int main(int argc, char *argv[])
{
  print_double_int(4, printf);
}

通常、引数は関数(プロシージャ、ブロック)の入力/出力パラメーターを指定するために使用されますが、グローバル名は「環境内に存在する」ライブラリー関数などを参照するために使用されるため、それらを言及する方が便利です。それらが必要な場合のみ。グローバル名の代わりに引数を使用することは、依存性注入の主な考え方です。これは、コンテキストを見ることで解決するのではなく、依存関係を明示する必要がある場合に使用されます。

外部で定義された名前の別の同様の使用は、クロージャーで見つけることができます。この場合、ブロックのレキシカルコンテキストで定義された名前はブロック内で使用でき、その名前にバインドされた値は、ブロックがそれを参照している限り、(通常)存在し続けます。

たとえば、次のScalaコードをご覧ください。

object ClosureExample
{
  def createMultiplier(n: Int) = (m: Int) => m * n

  def main(args: Array[String])
  {
    val multiplier3 = createMultiplier(3)
    val multiplier5 = createMultiplier(5)

    // Prints 6.
    println(multiplier3(2))

    // Prints 10.
    println(multiplier5(2))
  }
}

関数の戻り値はcreateMultiplierクロージャ(m: Int) => m * nで、引数mと外部名が含まれますn。名前nは、クロージャーが定義されているコンテキストを調べることで解決されnますcreateMultiplier。名前はfunction の引数にバインドされます。このバインディングは、クロージャーが作成されたとき、つまりcreateMultiplier呼び出されたときに作成されることに注意してください。したがって、名前nは、関数の特定の呼び出しの引数の実際の値にバインドされます。これを、printfプログラムの実行可能ファイルがビルドされたときにリンカーによって解決されるライブラリ関数の場合と比較してください。

要約すると、ローカルコードブ​​ロック内の外部名を参照すると便利です。

  • 引数として外部で定義された名前を明示的に渡す必要はありません。
  • ブロックの作成時に実行時にバインディングをフリーズし、後でブロックが呼び出されたときにバインディングにアクセスできます。

ブロックでは、printf使用する機能など、環境で定義されている関連する名前にのみ関心があると考えると、シャドーイングが発生します。偶然場合は、(ローカル名を使用したいgetcputcscanf無視する、...)は、すでに環境で使用されてきた、あなたの簡単な希望(影)グローバル名。そのため、ローカルで考える場合、全体(おそらく非常に大きな)コンテキストを考慮したくありません。

他の方向では、グローバルに考えるとき、ローカルコンテキストの内部の詳細(カプセル化)を無視する必要があります。したがって、シャドウイングが必要です。そうしないと、グローバル名を追加すると、すでにその名前を使用していたすべてのローカルブロックが破損する可能性があります。

結論として、コードのブロックが外部で定義されたバインディングを参照するようにする場合は、グローバル名からローカル名を保護するためにシャドウイングが必要です。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.