web-dev-qa-db-ja.com

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

最も一般的なプログラミング言語の多く(C++、Java、Pythonなど))には、変数のhiding/shadowingの概念があります。隠したり、隠したりしたときに、バグを見つけるのが困難になり、これらの言語の機能を使用する必要があると思ったことがありません。

私にとっては、非表示やシャドウイングを許可しない方が良いように思えます。

誰かがこれらの概念の適切な使用法を知っていますか?

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

31
Simon

非表示とシャドーイングを許可しない場合、使用できるのは、すべての変数がグローバルである言語です。

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

[〜#〜] and [〜#〜]特定のグローバル変数を「保護」しようとすると、コンパイラーがプログラマーに「ごめんなさい」と伝える状況が発生します。 、デイブ、その名前は使用できません。すでに使用されています。」 COBOLの経験から、プログラマーはこのような状況ではほとんどすぐに冒とく的な表現に頼っています。

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

27
John R. Strohm

誰かがこれらの概念の適切な使用法を知っていますか?

正確で記述的な識別子を使用することは、常に良い使用法です。

同じ/類似のタイプの2つの非常によく似た名前の変数(変数の非表示が許可されていない場合に行うこと)があると、同じくらい多くのバグが発生する可能性が高いため、変数の非表示によって多くのバグが発生することはないと主張できます深刻なバグ。その引数が正しいであるかどうかはわかりませんが、少なくとももっともらしい議論の余地があります。

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

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

15
Telastyn

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

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

ここに長い答えがあります。まず、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のインスタンスは、 "Bar"を生成する必要がある場合、プロパティMyFooStringに対して常に "Foo"を生成します。 IFooが具体的にはバーであることを知っているだけでなく、別のアクセサーを使用することも知っている必要があります。

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

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

この単純な例では、完全な答えです限り FooとBarが両方ともIFooであることを気にします。数例の使用法コードは、BarがFooではなく、そのように割り当てることができないため、コンパイルに失敗します。ただし、Barに必要な便利なメソッド「FooMethod」がFooにあった場合、そのメソッドを継承できなくなります。あなたはそのコードをバーに複製するか、創造的にする必要があります:

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

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

これは明らかなハックであり、O-O言語仕様の一部の実装はこれに過ぎませんが、概念的には間違っています。 BarのコンシューマーがFooの機能を公開する必要がある場合、Barはhave Fooではなくbe a Fooである必要があります。

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

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

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

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

要約すると、メンバーの非表示を許可することは、これらの悪の少ない方です。それを持たないことは、それを可能にするよりも、オブジェクト指向の原則に反する残虐行為を悪化させることになります。

7
KeithS

正直なところ、C#コンパイラチームの主な開発者であるEric Lippertは かなりよく説明しています (リンクを提供してくれたLescai Ionelに感謝します)。 .NETのIEnumerableおよび_IEnumerable<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()GetEnumeratorGeneric()とまったく同じですが、わずかに異なる戻り値。実際には非常に似ているため、以前のコードで作業しているのでない限り、常にはデフォルトでGetEnumeratorのジェネリック形式にしたいと思うでしょう。ジェネリックが.NETに導入される前に作成されました。

メンバーを非表示にする場合があると、厄介なコードや見つけにくいバグが発生する可能性が高くなります。ただし、レガシーコードを壊さずに戻り値の型を変更したい場合などに便利です。これは、言語設計者が下す必要のある決定の1つにすぎません。この機能を正当に必要とする開発者に迷惑をかけてそれを除外しますか、それともこの機能を言語に組み込んで、誤用の犠牲者からのフレークをキャッチしますか?

2
Phil

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

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

それは時々いくつかのバグにつながるかもしれませんが、それ以外の場合に起こり得る莫大な量のバグを防ぐことによってそれ以上に補償します。ライブラリ関数と同じ名前のローカル定義を作成する(それを行わないでください)以外に、ローカルスコープでバグを導入する簡単な方法はわかりませんが、ローカルスコープは同じプログラムの多くの部分で使用できますお互いを壊さずにループのインデックスカウンターとして使用し、同じ名前の文字列を破壊しないstrという名前の文字列を使用する関数をFredにホールに書きます。

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

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

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

2
Michael Shaw

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

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

ブロックで使用される名前は、次の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にバインドされます。このバインディングは、クロージャの作成時、つまりcreateMultiplierが呼び出されたときに作成されることに注意してください。したがって、名前nは、関数の特定の呼び出しの引数の実際の値にバインドされます。これをprintfのようなライブラリ関数の場合と比較してください。これは、プログラムの実行可能ファイルがビルドされるときにリンカーによって解決されます。

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

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

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

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

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

2
Giorgio