コンピュータプログラミングにおいて、名前バインディング(変数などのエンティティと名前の関連付け)のスコープとは、その名前バインディングが有効なプログラムの部分を指します。言い換えれば、スコープとは、名前を使ってエンティティを参照できる範囲のことです。プログラムの他の部分では、その名前は別のエンティティ(異なるバインディングを持つ)を参照したり、何も参照しなかったり(バインドされていない)する可能性があります。スコープは、同じ名前が異なるオブジェクトを参照できるようにすることで、名前の衝突を防ぐのに役立ちます。ただし、名前のスコープは別々である必要があります。名前バインディングのスコープは、特に古い文献やより技術的な文献では、エンティティの可視性とも呼ばれます。これは、参照するエンティティに関するものであり、参照する名前に関するものではありません。
「スコープ」という用語は、プログラムの一部またはプログラム内の特定の時点で有効なすべての名前バインディングの集合を指す場合にも使用され、これはより正確にはコンテキストまたは環境と呼ばれます。[ a ]
厳密に言えば[ b ] 、そしてほとんどのプログラミング言語では、実際には「プログラムの一部」とはソースコードの一部(テキスト領域)を指し、これはレキシカルスコープと呼ばれます。しかし、一部の言語では、「プログラムの一部」とは実行時間の一部(実行中の期間)を指し、これはダイナミックスコープと呼ばれます。これらの用語はどちらもやや誤解を招く可能性があります。定義で説明したように、これらは専門用語を誤用しているからです。しかし、区別自体は正確で明確であり、これらはそれぞれ標準的な用語です。この記事ではレキシカルスコープを主な焦点とし、ダイナミックスコープはレキシカルスコープとの対比として理解されます。
ほとんどの場合、字句スコープに基づく名前解決は、使用も実装も比較的簡単です。使用時にはソースコードを遡って名前がどのエンティティを参照しているかを判断でき、実装時にはプログラムのコンパイル時や解釈時に名前とコンテキストのリストを保持できます。名前マスキング、前方宣言、巻き上げでは困難が生じますが、非ローカル変数、特にクロージャでは、かなり微妙な問題が生じます。
名前(識別子)の(語彙的)「スコープ」の厳密な定義は明確です。語彙的スコープとは、「名前とエンティティの束縛が適用されるソースコードの部分」です。これは、 ALGOL 60の仕様における 1960 年の定義からほとんど変わっていません。代表的な言語仕様を以下に示します。
一般的に「スコープ」とは、特定の名前が特定の変数を参照できる場合、つまり宣言が有効になる場合を指しますが、関数、型、クラス、ラベル、定数、列挙型などの他のエンティティにも適用できます。
スコープの根本的な違いは、「プログラムの一部」が何を意味するかです。レキシカルスコープ(静的スコープとも呼ばれる)を持つ言語では、名前解決はソースコード内の位置とレキシカルコンテキスト(静的コンテキストとも呼ばれる)に依存します。レキシカルコンテキストは、名前付き変数または関数が定義されている場所によって定義されます。対照的に、動的スコープを持つ言語では、名前解決は、名前が見つかったときのプログラムの状態に依存します。プログラムの状態は、実行コンテキスト(ランタイムコンテキスト、呼び出しコンテキスト、または動的コンテキストとも呼ばれる)によって決定されます。実際には、レキシカルスコープでは、名前はローカルのレキシカルコンテキストを検索することによって解決され、それが失敗した場合は、外側のレキシカルコンテキストを検索することによって解決され、以下同様です。一方、動的スコープでは、名前はローカルの実行コンテキストを検索することによって解決され、それが失敗した場合は、外側の実行コンテキストを検索することによって解決され、以下同様で、呼び出しスタックを上に進みます。[ 4 ]
ほとんどの現代言語は変数と関数にレキシカルスコープを使用しますが、一部の言語、特にLispの一部のダイアレクト、一部の「スクリプト」言語、および一部のテンプレート言語では動的スコープが使用されます。[ c ] Perl 5はレキシカルスコープと動的スコープの両方を提供します。レキシカルスコープの変数を使用する関数はクロージャとして知られています。
字句解決はコンパイル時に決定できるため、早期バインディングとも呼ばれますが、動的解決は一般的に実行時にのみ決定できるため、遅延バインディングと呼ばれます。
オブジェクト指向プログラミングでは、動的ディスパッチによって実行時にオブジェクトメソッドが選択されますが、実際の名前バインディングがコンパイル時に行われるか実行時に行われるかは言語によって異なります。事実上の動的スコープはマクロ言語で一般的であり、マクロ言語は名前解決を直接行わず、代わりにその場で展開します。
AngularJSのようなプログラミング フレームワークでは、構文上「スコープ」という用語を、この記事で使用されている意味とは全く異なる意味で使用しています。これらのフレームワークでは、スコープは、使用しているプログラミング言語 ( AngularJS の場合はJavaScript ) のオブジェクトであり、フレームワークによって特定の方法で使用され、変数にレキシカル スコープを使用する言語で動的スコープをエミュレートします。これらのAngularJS スコープは、プログラムの任意の部分で、コンテキスト内にある場合も、コンテキスト内にない場合もあり (用語の通常の意味を使用)、他のオブジェクトと同様に言語の変数スコープの通常の規則に従い、独自の継承とトランスクルージョン規則を使用します。AngularJS のコンテキストでは、混乱を避けるために「$scope」(ドル記号付き) という用語が使用されることがありますが、変数名にドル記号を使用することは、スタイル ガイドで推奨されないことがよくあります。[ 5 ]
スコープは名前解決の重要な構成要素であり、名前解決は言語の意味論の基礎となります。 [ d ]名前解決(スコープを含む)はプログラミング言語によって異なり、また同じプログラミング言語内でもエンティティの種類によって異なります。スコープに関する規則はスコープ規則(またはスコープ規則)と呼ばれます。スコープ規則は名前空間とともにモジュール型プログラミングにおいて非常に重要であり、プログラムのある部分の変更が無関係な部分を壊さないようにします。
スコープについて議論する際には、スコープ、範囲、コンテキストという3 つの基本的な概念があります。特に「スコープ」と「コンテキスト」はよく混同されます。スコープは名前バインディングのプロパティであり、コンテキストはプログラムの一部のプロパティです。これは、ソース コードの一部 (字句コンテキストまたは静的コンテキスト) または実行時の一部(実行コンテキスト、実行時コンテキスト、呼び出しコンテキストまたは動的コンテキスト) のいずれかです。実行コンテキストは、字句コンテキスト (現在の実行ポイント) と、コール スタックなどの追加の実行時状態で構成されます。[ e ]厳密に言えば、実行中にプログラムはさまざまな名前バインディングのスコープに出入りし、実行のある時点で名前バインディングは「コンテキスト内」または「コンテキスト外」になります。したがって、プログラムの実行がスコープに出入りすると、名前バインディングは「コンテキスト内に入る」または「コンテキスト外になる」ことになります。[ f ]しかし、実際の使用法ははるかに緩やかです。
スコープはソースコードレベルの概念であり、名前バインディング、特に変数名や関数名のバインディングの特性です。ソースコード内の名前はプログラム内のエンティティへの参照であり、言語のコンパイラやインタプリタの動作の一部です。そのため、スコープの問題は、より一般的にプログラムで使用される参照の一種であるポインタに似ています。名前がコンテキスト内にあるにもかかわらず変数が初期化されていない状態で変数の値を使用することは、未定義であるため、ワイルドポインタの逆参照(値へのアクセス)に似ています。ただし、変数はコンテキストから外れるまで破棄されないため、ダングリングポインタに相当するものは存在しません。
変数などのエンティティの場合、スコープはライフタイム(範囲とも呼ばれる)のサブセットです。名前は存在する変数(未定義の値を持つ場合もある)のみを参照できますが、存在する変数が必ずしも可視であるとは限りません。変数は存在してもアクセスできない場合(値は格納されているが、特定のコンテキスト内では参照されない)、またはアクセス可能でも指定された名前ではアクセスできない場合があり、その場合はコンテキスト内にありません(プログラムは「名前のスコープ外」です)。その他の場合、「ライフタイム」は関係ありません。ラベル(ソースコード内の名前付き位置)は、(静的にコンパイルされた言語の場合)プログラムと同じライフタイムを持ちますが、プログラムの特定の時点でコンテキスト内にある場合とそうでない場合があります。静的変数についても同様です。静的グローバル変数はプログラム全体でコンテキスト内にありますが、静的ローカル変数は関数またはその他のローカルコンテキスト内でのみコンテキスト内にありますが、どちらもプログラムの実行全体にわたってライフタイムを持ちます。
名前がどのエンティティを参照しているかを判断することは、名前解決または名前バインディング(特にオブジェクト指向プログラミングにおいて)と呼ばれ、言語によって異なります。名前が与えられると、言語(正確にはコンパイラまたはインタプリタ)は、そのコンテキスト内のすべてのエンティティをチェックして一致するものを探します。曖昧さ(同じ名前の2つのエンティティ、例えば同じ名前のグローバル変数とローカル変数など)がある場合は、名前解決ルールを使用してそれらを区別します。名前解決は、多くの場合、PythonのLEGB(ローカル、囲み、グローバル、組み込み)ルールのような「内側から外側のコンテキスト」ルールに依存します。名前は暗黙的に最も狭い関連コンテキストに解決されます。Pythonのキーワードglobalやnonlocalキーワードのように、名前解決を明示的に指定できる場合もありますが、デフォルトのルールを上書きできない場合もあります。
同じ名前が2つ同時に存在し、それぞれ異なるエンティティを指している場合、名前マスキングが発生していると言われます。これは、優先順位の高い名前(通常は最も内側の名前)が、優先順位の低い名前を「隠蔽」している状態です。変数レベルでは、これは変数シャドウイングと呼ばれます。マスキングによって論理エラーが発生する可能性があるため、一部のプログラミング言語ではマスキングを禁止または推奨せず、コンパイル時または実行時にエラーや警告を発生させます。
様々なプログラミング言語には、宣言や名前の種類ごとに異なるスコープ規則があります。このようなスコープ規則は言語の意味論に大きな影響を与え、結果としてプログラムの動作や正しさにも影響を及ぼします。C ++のような言語では、未定義変数へのアクセスは明確な意味論を持たず、ダングリングポインタを参照した場合と同様に未定義動作を引き起こす可能性があります。また、スコープ外で使用される宣言や名前は構文エラーを生成します。
スコープは他の言語構造と結びついて暗黙的に決定されることが多いが、多くの言語ではスコープを制御するための専用の構造も提供されている。
スコープは、単一の式からプログラム全体まで、さまざまな段階に分けられます。最も単純なスコープ規則はグローバルスコープで、すべてのエンティティがプログラム全体で参照可能です。最も基本的なモジュールスコープ規則は2レベルスコープで、プログラム内のどこでもグローバルスコープ、関数内ではローカルスコープとなります。より高度なモジュールプログラミングでは、モジュールスコープを別途設けることができ、モジュール内では名前が参照可能(モジュール内のみ)ですが、外部からは参照できません。関数内では、C言語などの一部の言語では、ブロックスコープを使用してスコープを関数のサブセットに制限できます。また、関数型プログラミング言語などの他の言語では、式スコープを使用してスコープを単一の式に制限できます。その他のスコープには、モジュールスコープと同様の動作をするファイルスコープ(特にC言語)や、関数外のブロックスコープ(特にPerl)などがあります。
微妙な問題は、スコープがいつ始まり、いつ終わるかということです。C などの一部の言語では、名前のスコープは名前の宣言から始まるため、特定のブロック内で宣言された異なる名前は異なるスコープを持つことができます。これは、関数を使用する前に宣言する必要がありますが、必ずしも定義する必要はありません。また、相互再帰など、場合によっては前方宣言が必要になります。Python などの他の言語では、名前のスコープは、名前が宣言されている関連ブロックの開始 (関数の開始など) から始まり、どこで定義されているかに関係なく、特定のブロック内のすべての名前は同じスコープを持ちます。JavaScript では、またはで宣言された名前のスコープはlet名前constの宣言から始まり、で宣言された名前のスコープは、var名前が宣言されている関数の開始から始まります。これは変数巻き上げとして知られています。コンテキスト内で未定義の値を持つ名前の動作は異なります。Python では、未定義の名前を使用すると実行時エラーになりますが、JavaScript では、で宣言された未定義の名前はvar暗黙的に値にバインドされるため、関数全体で使用できますundefined。
名前束縛のスコープは式であり、これは式スコープと呼ばれます。式スコープは多くの言語、特に関数型言語で利用可能であり、関数型言語では、宣言のスコープを単一の式にできるlet式と呼ばれる機能を提供します。これは、たとえば計算の中間値が必要な場合に便利です。たとえば、Standard MLでは、f()がを返す場合12、は、という名前の一時変数を使用してを2回呼び出さないようにしてに評価される式です。ブロックスコープを持つ一部の言語では、ブロックを式に埋め込む構文を提供することで、この機能を近似しています。たとえば、前述のStandard ML式は、 Perlではとして、GNU Cではとして記述できます。let val x = f() in x * x end144xf()do{my$x=f();$x*$x}({intx=f();x*x;})
Pythonでは、ジェネレータ式およびリスト内包表記(Python 3以降)における補助変数は、式スコープを持ちます。
C言語では、関数プロトタイプ内の変数名は式スコープを持ち、この文脈では関数プロトコルスコープと呼ばれます。プロトタイプ内の変数名は参照されないため(実際の定義では異なる場合もあります)、単なるダミー変数として扱われ、省略されることが多いですが、例えばドキュメント生成などに使用されることがあります。
名前束縛のスコープはブロックであり、これはブロックスコープとして知られています。ブロックスコープは、ブロック構造のプログラミング言語の多くで利用できますが、すべてではありません。これはALGOL 60で始まり、「すべての宣言は、そのブロックに対してのみ有効である」[ 6 ]と規定され、今日では特にPascalやC系の言語と関連付けられています。多くの場合、このブロックは関数内に含まれており、スコープは関数の一部に制限されますが、Perlなどの一部の言語では、ブロックが関数内に含まれない場合があります。
int sum_of_squares ( int m ) { int result = 0 ; for ( int n = 1 ; n <= m ; ++ n ) { const int n_squared = n * n ; result += n_squared ; } return result ; }ブロックスコープの使用例として、ここに示したCコードが挙げられます。このコードでは、2つの変数がループスコープに指定されています。1つはループ変数nで、一度初期化され、ループの各イテレーションでインクリメントされます。もう1つは補助変数n_squaredで、これも各イテレーションで初期化されます。この目的は、特定のブロックにのみ関連する変数を関数スコープに追加しないようにすることです。例えば、汎用ループ変数iが誤って別の値に設定されている場合などに発生するエラーを防ぐことができます。この例では、式はn * n通常補助変数に代入されず、ループ本体は単純に記述されますresult += n * nが、より複雑な例では補助変数が役立ちます。
ブロックは主に制御フローに使用され、if、while、forループなどが含まれます。これらの場合、ブロックスコープとは、変数のスコープが関数の実行フローの構造に依存することを意味します。ただし、ブロックスコープを持つ言語では、通常、「ネイキッド」ブロックの使用も許可されています。ネイキッドブロックの唯一の目的は、変数のスコープをきめ細かく制御することです。たとえば、補助変数をブロック内で定義し、使用(たとえば、関数スコープの変数に追加)してから、ブロックの終了時に破棄することができます。また、whileループを、ループ内で使用される変数を一度だけ初期化するブロックで囲むこともできます。
Algol 68やC(この例で示され、C99以降標準化されている)など、いくつかのプログラミング言語の微妙な特徴として、ブロックスコープ変数はブロック本体内だけでなく、制御文(存在する場合)内にも宣言できる点が挙げられます。これは関数パラメータに類似しており、関数パラメータは関数宣言(関数本体のブロックが始まる前)で宣言され、関数本体全体にわたってスコープを持ちます。これは主にforループで使用され、whileループとは異なり、ループ条件とは別に初期化文を持つため、一般的なイディオムとなっています。
ブロックスコープはシャドウイングに使用できます。この例では、ブロック内で補助変数をn と名付けてパラメータ名をシャドウイングすることもできますが、エラーが発生する可能性があるため、これは好ましくないスタイルとされています。さらに、Java や C# など、C の派生言語の中には、ブロックスコープをサポートしているにもかかわらず (ローカル変数を関数の終了前にコンテキストから外すことができる)、あるローカル変数が別のローカル変数を隠蔽することを許可していないものがあります。このような言語では、2 番目のnを宣言しようとすると構文エラーになり、n変数のいずれかの名前を変更する必要があります。
If a block is used to set the value of a variable, block scope requires that the variable be declared outside of the block. This complicates the use of conditional statements with single assignment. For example, in Python, which does not use block scope, one may initialize a variable as such:
ifc:a="foo"else:a=""where a is accessible after the if statement.
In Perl, which has block scope, this instead requires declaring the variable prior to the block:
my$a;if(c){$a='foo';}else{$a='';}Often this is instead rewritten using multiple assignment, initializing the variable to a default value. In Python (where it is not necessary) this would be:
a=""ifc:a="foo"while in Perl this would be:
my$a='';if(c){$a='foo';}In case of a single variable assignment, an alternative is to use the ternary operator to avoid a block, but this is not in general possible for multiple variable assignments, and is difficult to read for complex logic.
This is a more significant issue in C, notably for string assignment, as string initialization can automatically allocate memory, while string assignment to an already initialized variable requires allocating memory, a string copy, and checking that these are successful.
{my$counter=0;subincrement_counter{return++$counter;}}Some languages allow the concept of block scope to be applied, to varying extents, outside of a function. For example, in the Perl snippet at right, $counter is a variable name with block scope (due to the use of the my keyword), while increment_counter is a function name with global scope. Each call to increment_counter will increase the value of $counter by one, and return the new value. Code outside of this block can call increment_counter, but cannot otherwise obtain or alter the value of $counter. This idiom allows one to define closures in Perl.
関数内で宣言された変数のスコープがその関数を超えて拡張されない場合、これは関数スコープと呼ばれます。[ 7 ]関数スコープは、関数またはサブルーチン内でローカル変数を作成する方法を提供するほとんどのプログラミング言語で使用できます。ローカル変数とは、関数が戻るときにスコープが終了する(コンテキストから外れる)変数です。ほとんどの場合、変数の寿命は関数呼び出しの期間です。これは自動変数であり、関数の開始時(または変数が宣言されたとき)に作成され、関数が戻るときに破棄されます。一方、変数のスコープは関数内ですが、「内」の意味はスコープがレキシカルか動的かによって異なります。ただし、C などの一部の言語では、静的ローカル変数も提供されています。静的ローカル変数の寿命はプログラム全体の寿命ですが、変数は関数内のみでコンテキストにあります。静的ローカル変数の場合、変数はプログラムの初期化時に作成され、プログラムが終了するときにのみ破棄されます。これは静的グローバル変数と同様ですが、自動ローカル変数と同様に、関数内のみでコンテキストにあります。
重要な点として、レキシカルスコープでは、関数スコープを持つ変数は、関数のレキシカルコンテキスト内でのみスコープを持ちます。関数内で別の関数が呼び出されるとコンテキストから外れ、関数が戻るとコンテキストに戻ります。呼び出された関数は呼び出し元の関数のローカル変数にアクセスできず、ローカル変数は宣言された関数の本体内でのみコンテキストを持ちます。対照的に、動的スコープでは、スコープは関数の実行コンテキストにまで広がります。ローカル変数は別の関数が呼び出されてもコンテキスト内に留まり、定義した関数が終了するとコンテキストから外れるため、ローカル変数は定義された関数と呼び出されたすべての関数のコンテキスト内にあります。レキシカルスコープとネストされた関数を持つ言語では、ローカル変数はネストされた関数に対してはコンテキスト内にありますが、レキシカルにネストされていない他の関数に対してはコンテキスト内にありません。囲んでいる関数のローカル変数は、ネストされた関数に対しては非ローカル変数として知られています。関数スコープは匿名関数にも適用されます。
def square ( n : int ) -> int : return n * ndef sum_of_squares ( n : int ) -> int : total : int = 0 i : int = 0 while i <= n : total += square ( i ) i += 1 return total例えば、右側のPythonコードの抜粋では、2つの関数が定義されています。squareと ですsum_of_squares。squareは数値の2乗を計算し、はsum_of_squares数値までのすべての2乗の合計を計算します。(例えば、square(4)は 4 2 = 、は 0 2 + 1 2 + 2 2 + 3 2 + 4 2 =です。) 16sum_of_squares(4) 30
これらの各関数には、関数の引数を表すnという名前の変数があります。これら 2 つのn変数は、同じ名前であっても、完全に独立しており、関連性はありません。これは、それぞれが関数スコープを持つレキシカルスコープのローカル変数であるためです。つまり、それぞれのスコープは独自のレキシカル分離関数であり、重複しません。したがって、 は自身のn を変更することなくsum_of_squaresを呼び出すことができます。同様に、 にはtotalとiという名前の変数があります。これらの変数は、スコープが限定されているため、他の関数に属する可能性のあるtotalまたはiという名前の変数と干渉しません。言い換えれば、これらの名前と無関係な名前が同一であっても、名前の衝突のリスクはありません。squaresum_of_squares
スコープが重複しないため、名前のマスキングは発生しません。常にnという名前の変数が1つだけコンテキスト内に存在することになります。これに対し、同様のコード断片を動的スコープを持つ言語で記述した場合、呼び出し元の関数内のnは呼び出された関数内でもコンテキスト内に残り(スコープが重複する)、呼び出された関数内の新しいnによってマスキング(「シャドウイング」)されます。
関数が第一級オブジェクトであり、関数によってローカルに作成されて返される場合、関数のスコープは著しく複雑になります。この場合、ネストされた関数内のローカルでない変数(関数定義内の非バインド変数で、外側のコンテキストの変数に解決されるもの)はクロージャを形成します。これは、関数自体だけでなく、そのコンテキスト(変数)も返され、その後、別のコンテキストで呼び出される可能性があるためです。これにはコンパイラによる大幅なサポートが必要となり、プログラム解析が複雑になる可能性があります。
名前バインディングのスコープはファイルであり、これはファイルスコープと呼ばれます。ファイルスコープは主にC(およびC++)に特有のもので、ファイルの最上位レベルで宣言された変数と関数(関数内ではない)のスコープはファイル全体、つまりCの場合は宣言からソースファイルの末尾、より正確には翻訳単位(内部リンク)までとなります。これはモジュールスコープの一種と見なすことができ、モジュールはファイルと関連付けられます。より現代的な言語では、明示的なモジュールスコープに置き換えられています。内部コンテキストに変数と関数を追加し、さらに別のインクルード文を呼び出す可能性のあるインクルード文が存在するため、ファイル本体で何がコンテキスト内にあるかを判断するのは難しい場合があります。
上記のC言語コードでは、関数名はsum_of_squaresグローバルスコープ(C言語では外部リンケージ)を持ちます。static関数シグネチャに を追加すると、ファイルスコープ(内部リンケージ)になります。
名前バインディングのスコープはモジュールであり、これはモジュールスコープと呼ばれます。モジュールスコープは、モジュール(複数のファイルにまたがる場合がある)が複雑なプログラムの基本単位となるモジュール型プログラミング言語で利用できます。モジュールスコープは、情報の隠蔽と限定的なインターフェースの公開を可能にします。モジュールスコープはModulaファミリーの言語で先駆的に導入され、Modulaの影響を受けたPythonは、現代における代表的な例です。
C++20以前の C++ など、モジュールを直接サポートしていないオブジェクト指向プログラミング言語では、[ 8 ]代わりにクラス階層によって同様の構造が提供され、クラスがプログラムの基本単位となり、クラスはプライベート メソッドを持つことができます。これは、名前解決やスコープではなく、動的ディスパッチの文脈で正しく理解されますが、多くの場合、同様の役割を果たします。Python のように、モジュールとクラスの両方を持つ場合、これらの両方の機能が利用できる場合があり、コードの構成 (モジュール レベルの関数または従来のプライベート メソッドとして) はプログラマの選択となります。
名前バインディングのスコープはプログラム全体であり、これはグローバルスコープと呼ばれます。グローバルスコープを持つ変数名(グローバル変数と呼ばれる)は、少なくとも一部の言語では、名前の衝突や意図しないマスキングの可能性、モジュール性の低さから、好ましくない慣習とみなされることが多く、関数スコープやブロックスコープが推奨されます。しかし、グローバルスコープは(言語によって異なりますが)、関数名、クラス名、その他のデータ型名など、さまざまな種類の名前に一般的に使用されます。このような場合、名前空間などのメカニズムを使用して衝突を回避します。
ローカル変数(特定の関数内でのみ存在する、スコープが限定された変数名)を使用することで、同じ名前の変数同士が衝突するリスクを回避できます。しかし、この疑問に答えるには、大きく異なる2つのアプローチがあります。「関数内」とはどういう意味でしょうか?
字句スコープ(または字句スコープ、静的スコープとも呼ばれる)では、変数名のスコープが特定の関数である場合、そのスコープは関数定義のプログラムテキストになります。そのテキスト内では変数名が存在し、変数の値にバインドされますが、そのテキストの外では変数名は存在しません。対照的に、動的スコープ(または動的スコープ)では、変数名のスコープが特定の関数である場合、そのスコープは関数が実行されている期間になります。関数が実行されている間は変数名が存在し、その値にバインドされますが、関数が戻ると変数名は存在しません。これは、関数が別々に定義された関数を呼び出す場合、字句スコープでは関数はのローカル変数にアクセスできません(のテキストがのテキスト内にないと仮定した場合)。一方、動的スコープでは、関数はのローカル変数にアクセスできます(の呼び出し中にが呼び出されるため)。fggfgfgfgf
$ # bash 言語$ x = 1 $ function g () { echo $x ; x = 2 ; } $ function f () { local x = 3 ; g ; } $ f # これは 1 と 3 のどちらを出力しますか? 3 $ echo $x # これは 1 と 2 のどちらを出力しますか? 1例えば、右側のプログラムを考えてみましょう。最初の行 は、グローバル変数を作成し、 に初期化します。2行目 は、の現在の値を出力(「エコー」)し、を に設定する(以前の値を上書きする)関数を定義します。3行目 は、ローカル変数を作成し(同じ名前のグローバル変数を隠蔽)、 に初期化し、 を呼び出す関数を定義します。4行目 はを呼び出します。5行目 は、 の現在の値を出力します。x=1x1functiong(){echo$x;x=2;}gxx2functionf(){localx=3;g;}fx3gffecho$xx
スコープ規則によって、このプログラムが出力する内容が決まります。このプログラムの言語が字句スコープを使用する場合、 はの外部で定義されているためg、 を出力してグローバル変数を変更します。そのため、プログラムは を出力し、次に を出力します。一方、この言語が動的スコープを使用する場合、 は の内部から呼び出されているため、 を出力してのローカル変数を変更します。そのため、プログラムは を出力し、次に を出力します。(ちなみに、このプログラムの言語は動的スコープを使用するBashです。そのため、プログラムは を出力し、次に を出力します。同じコードを字句スコープを使用するksh93で実行した場合、結果は異なります。)xgf12gfxgf3131
字句スコープでは、名前は常にその字句コンテキストを参照します。これはプログラムテキストの特性であり、言語実装によって実行時コールスタックから独立しています。このマッチングは静的プログラムテキストの解析のみを必要とするため、このタイプのスコープは静的スコープとも呼ばれます。字句スコープは、 Pascal、Modula-2、AdaなどのALGOLベースの言語、およびMLやHaskellなどの現代的な関数型言語で標準となっています。C言語とその構文的および意味論的な関連言語でも使用されていますが、制限の種類が異なります。静的スコープでは、プログラマはパラメータ、変数、定数、型、関数などのオブジェクト参照を単純な名前置換として推論できます。これにより、ローカル命名構造を独立して理解できるため、モジュール化されたコードを作成し、推論することがはるかに容易になります。対照的に、動的スコープでは、プログラマはモジュールのコードが呼び出される可能性のあるすべての実行コンテキストを予測する必要があります。
プログラムP ;変数I :整数; K :文字;手順A ; var K :実数; L :整数;手続きB ;変数M :実数;開始(*スコープ P+A+B*)終了;(*scope P+A*) end ;(*スコープ P*)終了。例えば、Pascal は字句スコープです。右の Pascal プログラム断片を考えてみましょう。変数 は、I同じ名前の別の変数によって隠されることがないため、すべてのポイントで可視です。char変数 は、プロシージャと でのみ可視な変数Kによって隠されているため、メイン プログラムでのみ可視です。変数もプロシージャ と でのみ可視ですが、他の変数を隠していません。変数 はプロシージャ でのみ可視であるため、プロシージャからもメイン プログラムからもアクセスできません。また、プロシージャ はプロシージャ でのみ可視であるため、メイン プログラムから呼び出すことはできません。プロシージャ の外側で、プログラム内にという名前の別のプロシージャが宣言されていた可能性があります。プログラム内で が言及されている場所によって、変数のスコープと同様に、という名前の 2 つのプロシージャのうちどちらを表すかが決まります。realKABLABMBABABBBB
第一級のネスト関数を持つ言語でレキシカルスコープを正しく実装することは容易ではありません。各関数値には、依存する変数の値の記録が伴う必要があるためです (関数とこのコンテキストのペアはクロージャと呼ばれます) 。実装とコンピュータアーキテクチャによっては、レキシカルに深くネストされた関数を使用すると、変数検索が わずかに非効率になる可能性がありますが、これを軽減するためのよく知られた手法があります。[ 9 ] [ 10 ]また、自身の引数と (直近の) ローカル変数のみを参照するネスト関数の場合、すべての相対位置はコンパイル時にわかります。したがって、このタイプのネスト関数を使用する場合、オーバーヘッドは全く発生しません。ネスト関数が使用されていないプログラムの特定の部分、および当然ながら、ネスト関数が利用できない言語 (C 言語など) で書かれたプログラムにも同じことが当てはまります。
語彙スコープは、1960年代初頭に命令型言語ALGOL 60で初めて使用され、それ以来、他のほとんどの命令型言語で採用されています。[ 4 ]
PascalやCのような言語は、 ALGOL 60やALGOL 68の考え方に影響を受けているため、常にレキシカルスコープを備えていました(ただし、Cにはレキシカルネストされた関数は含まれていませんでした)。
Perlは、動的スコープを基本とする言語であり、後に静的スコープが追加された。
オリジナルのLispインタープリタ(1960年)は動的スコープを使用していた。静的(字句)スコープに近似するディープバインディングは、1962年頃のLISP 1.5で導入された(ジョン・マッカーシーの下で働いていたスティーブ・ラッセルが開発したFunargデバイスによる)。
初期のLispはすべて、インタプリタに基づいていたため、動的スコープを使用していました。1982年、Guy L. Steele Jr.とCommon Lispグループは『Common Lispの概要』 [ 11 ]を出版しました。これは、当時のLispの歴史と多様な実装の簡単な概説、およびCommon Lispの実装が備えるべき機能の概説です。102ページには次のように書かれています。
ほとんどのLISP実装は、デフォルトではインタプリタとコンパイラが正しいプログラムに対して異なる意味論を割り当てる可能性があるため、内部的に矛盾を抱えています。これは主に、インタプリタがすべての変数を動的スコープと想定するのに対し、コンパイラは特に指定しない限りすべての変数をローカル変数と想定していることに起因します。これは利便性と効率性を考慮して行われてきましたが、非常に微妙なバグにつながる可能性があります。Common LISPの定義では、インタプリタとコンパイラが正しいプログラムに対して同一の意味論を適用することを明示的に要求することで、このような異常を回避しています。
したがって、Common Lispの実装には字句スコープが必要でした。これもまた、Common Lispの概要から引用します。
さらに、Common LISP では、以下の機能 (そのほとんどは MacLisp、InterLisp、または Lisp Machines Lisp から借用) が提供されています。 (...) 完全な字句スコープの変数。いわゆる「FUNARG 問題」[ 12 ] [ 13 ]は、下方と上方の両方の場合において完全に解決されています。
『Common LISPの概要』が出版されたのと同じ年(1982年)には、コンパイル型で字句スコープを持つLispであるSchemeの初期設計(これもGuy L. Steele Jr.によるもの)が発表され、コンパイラによる実装が試みられていた。当時、Lispにおける字句スコープは実装効率が悪いと一般的に懸念されていた。『Tの歴史』[ 14 ]の中で、Olin Shiversは次のように書いている。
当時実用的に使われていた本格的なLispはすべて動的スコープでした。Rabbit [ 15 ]論文(1978年にガイ・ルイス・スティール・ジュニアによって書かれた)を注意深く読んでいなかった人は、レキシカルスコープがうまくいくとは信じていませんでした。それを読んだ数少ない人でさえ、これが本格的な実用でうまくいくと信じるのは少しばかりの飛躍でした。
「語彙スコープ」という用語は少なくとも1967年に遡り[ 16 ] 、 「語彙スコープ」という用語は少なくとも1970年に遡り、Project MACでLisp方言MDL(当時は「Muddle」として知られていた)のスコープ規則を説明するために使用された[ 17 ] 。
現代のプログラミング言語では、レキシカルスコープが関数型プログラミングパラダイムを実現する上で重要な役割を果たしています。JavaScript [ 18 ]、Python [ 19 ]、Swift [ 20 ]などの言語は、関数がレキシカルスコープ外で実行された場合でも、定義コンテキストから変数にアクセスできるようにするために、レキシカルスコープに大きく依存しています。これは、レキシカルスコープの直接的な結果であるクロージャを扱う場合に特に重要です。
例えば、JavaScriptでは、クロージャは非同期プログラミングやイベント処理によく使われます。クロージャを使うと、コールバックが外側のスコープの変数にアクセスできるため、よりクリーンでモジュール化されたコードが書けます。同様に、PythonやSwiftでは、レキシカルスコープを使ってクロージャを実装し、高階関数などの強力なパターンを実現しています。[ 21 ]
動的スコープでは、名前は実行コンテキストを参照します。技術的に言えば、これは各名前がバインディングのグローバルスタックを持つことを意味します。名前を持つローカル変数を導入すると、xバインディングがグローバルxスタック(空である場合もあります)にプッシュされ、制御フローがスコープを抜けるとポップされます。どのコンテキストで評価しても、常に最上位のバインディングが返されます。バインディングスタックは実行時xにのみ存在するため、コンパイル時にはこの処理は実行できないことに注意してください。これが、このタイプのスコープが動的スコープと呼ばれる理由です。
現代の言語では動的スコープは一般的ではない。[ 4 ]
一般的に、特定のブロックは、そのブロックの実行時間と同じライフタイムを持つバインディングを作成するように定義されます。これにより、動的スコープのプロセスに静的スコープの機能がいくつか追加されます。ただし、コードの一部はさまざまな場所や状況から呼び出される可能性があるため、変数が使用されるときにどのバインディングが適用されるか(またはそもそも存在するかどうか)を最初に判断することは困難です。これは有益です。最小知識の原則を適用すると、コードは変数の値の理由(または状況)に依存することを避け、変数の定義に従って値を使用するように提案されます。共有データのこの狭い解釈により、関数の動作をシステムの現在の状態(またはポリシー)に適応させるための非常に柔軟なシステムが提供されます。ただし、この利点は、このように使用されるすべての変数の慎重なドキュメント化と、変数の動作に関する仮定の慎重な回避に依存しており、プログラムの異なる部分間の干渉を検出するメカニズムは提供されません。PerlやCommon Lispなどの一部の言語では、プログラマが変数を定義または再定義するときに、静的スコープまたは動的スコープを選択できます。動的スコープを使用する言語の例としては、Logo、Emacs Lisp、LaTeX、およびシェル言語のbash、dash、PowerShellなどがあります。
動的スコープは実装が非常に簡単です。名前の値を見つけるには、プログラムは実行時スタックを走査し、各アクティベーションレコード(各関数のスタックフレーム)でその名前の値をチェックします。実際には、名前/値のペアのスタックである関連付けリストを使用することで、この処理はより効率的になります。ペアは宣言が行われるたびにこのスタックにプッシュされ、変数がコンテキストから外れるとポップされます。[ 22 ]浅いバインディングは、中央参照テーブルを使用して各名前を意味のスタックに関連付ける、はるかに高速な代替戦略です。これにより、実行時に特定の名前を見つけるための線形検索が回避されますが、このテーブルを適切に維持するように注意する必要があります。[ 22 ]これらの戦略はいずれも、任意の1つの変数のバインディングに対して後入れ先出し(LIFO)順序を前提としていることに注意してください。実際には、すべてのバインディングがそのように順序付けられています。
さらに簡単な実装は、単純なグローバル変数で動的変数を表現することです。ローカルバインディングは、プログラムからは見えないスタック上の匿名の場所に元の値を保存することによって行われます。そのバインディングスコープが終了すると、この場所から元の値が復元されます。実際、動的スコープはこのようにして生まれました。Lisp の初期の実装では、ローカル変数を実装するためにこの明白な戦略が使用され、GNU Emacs Lisp など、現在も使用されているいくつかの方言でこの慣習が残っています。レキシカルスコープは後に Lisp に導入されました。これは上記の浅いバインディング方式と同等ですが、中央の参照テーブルは単にグローバル変数のバインディングコンテキストであり、変数の現在の意味はそのグローバル値です。グローバル変数の維持は複雑ではありません。たとえば、シンボルオブジェクトは、そのグローバル値専用のスロットを持つことができます。
動的スコープはスレッドローカルストレージの優れた抽象化を提供しますが、そのように使用する場合は、グローバル変数の保存と復元に基づくことはできません。考えられる実装戦略は、各変数にスレッドローカルキーを持たせることです。変数にアクセスすると、スレッドローカルキーを使用してスレッドローカルメモリ位置にアクセスします(コンパイラによって生成されたコードにより、どの変数が動的でどの変数がレキシカルであるかがわかります)。呼び出し元のスレッドにスレッドローカルキーが存在しない場合は、グローバル位置が使用されます。変数がローカルにバインドされると、以前の値はスタック上の隠し場所に格納されます。スレッドローカルストレージは変数のキーの下に作成され、新しい値がそこに格納されます。そのスレッド内で変数をさらにネストしてオーバーライドすると、このスレッドローカル位置が保存され、復元されます。最初の最も外側のオーバーライドのコンテキストが終了すると、スレッドローカルキーが削除され、変数のグローバルバージョンが再びそのスレッドに公開されます。
参照透過性では、動的スコープは現在の関数の引数スタックのみに制限され、字句スコープと一致します。
現代のプログラミング言語において、プリプロセッサにおけるマクロ展開は、事実上の動的スコープの重要な例です。マクロ言語自体はソースコードを変換するだけで、名前の解決は行いませんが、展開はインプレースで行われるため、展開されたテキスト内の名前(特に自由変数)が解決される際には、動的スコープが実際に発生しているかのように、展開された場所(大まかに言えば「呼び出された場所」)に基づいて解決されます。
マクロ展開に使用されるCプリプロセッサは、名前解決を自身で行わず、マクロが定義されている場所に依存しないため、事実上動的スコープを持ちます。たとえば、次のマクロです。
#define ADD_A(x) x + aaマクロは渡された変数に追加されるように展開されますが、この名前はマクロADD_Aが「呼び出される」(正しく展開される)場所に基づいてコンパイラによって後で解決されます。正しくは、Cプリプロセッサは字句解析のみを行い、トークン化段階でマクロを展開しますが、構文木への解析や名前解決は行いません。
例えば、以下のコードでは、aマクロ内の名前は(展開後)展開箇所のローカル変数に解決されます。
#define ADD_A(x) (x + a)void add_one ( int * x ) { const int a = 1 ; * x = ADD_A ( * x ); }void add_two ( int * x ) { const int a = 2 ; * x = ADD_A ( * x ); }これまで見てきたように、スコープの重要な理由の一つは、同じ名前が異なるものを参照できるようにすることで、名前の衝突を防ぐのに役立つことです。ただし、名前にはそれぞれ異なるスコープが必要です。この制約は時に不便です。プログラム全体で多くの異なるものにアクセスする必要がある場合、それらはすべてグローバルスコープを持つ名前を必要とするため、名前の衝突を回避するには別の手法が必要になります。
この問題を解決するために、多くのプログラミング言語ではグローバル名を整理する仕組みが用意されています。これらの仕組みの詳細や使用される用語は言語によって異なりますが、基本的な考え方としては、名前のグループ自体に名前(接頭辞)を付けることができ、必要に応じて、その名前と接頭辞を組み合わせた修飾名でエンティティを参照できるというものです。通常、このような名前には、ある意味で2つのスコープセットがあります。1つは修飾名が見えるスコープ(通常はグローバルスコープ)、もう1つは修飾されていない名前(接頭辞なし)も見える1つ以上のより狭いスコープです。そして通常、これらのグループ自体もグループに整理することができ、つまりネストすることができます。
多くの言語がこの概念をサポートしていますが、その詳細は大きく異なります。C ++やC#のネームスペースのように、グローバル名をグループに整理することをほぼ唯一の目的とするメカニズムを持つ言語もあります。AdaのパッケージやStandard MLの構造体のように、これに加えて、グループ内の他のメンバーからのみ特定の名前が見えるようにする機能を持つ言語もあります。また、オブジェクト指向言語では、クラスやシングルトンオブジェクトがこの目的を果たすことを許可している場合が多くあります(ただし、これが主な目的となるメカニズムを持っているかどうかは別です)。さらに、これらのアプローチを融合させた言語も多くあります。例えば、PerlのパッケージはC++のネームスペースとほぼ同じですが、オブジェクト指向プログラミングではクラスとしても機能します。Javaは変数と関数をクラスに整理しますが、それらのクラスをAdaのようなパッケージに整理します。
代表言語に関する適用範囲規則は以下のとおりです。
C言語では、スコープは従来、特に変数に関してリンケージまたは可視性として知られています。C言語は、グローバルスコープ(外部リンケージ)、モジュールスコープまたはファイルスコープの一種(内部リンケージ)、およびローカルスコープ(関数内)を持つ字句スコープ言語です。関数内では、スコープはブロックスコープによってさらにネストできます。ただし、標準C言語は関数のネストをサポートしていません。
変数の寿命と可視性は、そのストレージクラスによって決まります。C には、静的 (プログラム実行)、自動 (ブロック実行、スタックに割り当てられる)、手動 (ヒープに割り当てられる) の 3 種類の寿命があります。変数に対してサポートされ、コンパイラによって処理されるのは静的と自動のみですが、手動で割り当てられたメモリは、異なる変数間で手動で追跡する必要があります。C には、外部リンケージ (グローバル)、内部リンケージ (おおよそファイル)、ブロック スコープ (関数を含む) の 3 つの可視性レベルがあります。ブロック スコープはネストでき、インクルードを使用することで、異なるレベルの内部リンケージが可能です。C の内部リンケージは、翻訳単位レベル、つまり、 C プリプロセッサによって処理された後のソース ファイル(特に、関連するすべてのインクルードを含む) での可視性です。
C言語プログラムは個別のオブジェクトファイルとしてコンパイルされ、その後リンカによって実行可能ファイルまたはライブラリにリンクされます。そのため、名前解決はコンパイラ(翻訳単位(より広義には「コンパイル単位」ですが、これは厳密には別の概念です)内で名前を解決する)とリンカ(翻訳単位をまたいで名前を解決する)に分かれています。詳細については、リンケージの項を参照してください。
C言語では、ブロックスコープを持つ変数は、宣言時にコンテキストに入り(ブロックの先頭ではなく)、ブロック内で(ネストされていない)関数が呼び出されるとコンテキストから外れ、関数が戻るとコンテキストに戻り、ブロックの終了時にコンテキストから外れます。自動ローカル変数の場合、宣言時に割り当てられ、ブロックの終了時に解放されます。一方、静的ローカル変数は、プログラムの初期化時に割り当てられ、プログラムの終了時に解放されます。
以下のプログラムは、ブロックスコープを持つ変数がブロックの途中でコンテキストに入り、ブロックが終了するとコンテキストから抜ける(そして実際に解放される)様子を示しています。
#include <stdio.h>int main ( void ) { char x = 'm' ; printf ( "%c \n " , x );{ printf ( "%c \n " , x ); char x = 'b' ; printf ( "%c \n " , x ); }printf ( "%c \n " , x ); }プログラムの出力:
m m b m
C言語には他にもスコープのレベルがあります。[ 23 ]関数プロトタイプで使用される変数名は、関数プロトタイプの可視性を持ち、関数プロトタイプの終了時にコンテキストを終了します。この名前は使用されないため、コンパイルには役立ちませんが、ドキュメント作成には役立つ場合があります。GOTO文のラベル名は関数スコープを持ちます。
プログラムで使用するすべての変数は、コード内のより前の箇所で型指定子とともに宣言されている必要があります。これは、関数本体の冒頭で、maina、b、resultがint型であることを宣言した際に行われたのと同様です。変数は、グローバルスコープまたはローカルスコープのいずれかになります。グローバル変数は、すべての関数の外側にあるソースコードの本体で宣言された変数であり、ローカル変数は、関数またはブロックの本体内で宣言された変数です。
最新バージョンでは、入れ子になった語彙スコープが使用可能です。
Javaは字句スコープを持つ。
Javaクラスにはいくつかの種類の変数があります: [ 24 ]
一般的に、括弧のセットは特定のスコープを定義しますが、クラス内のトップレベルの変数は、定義で使用される修飾子キーワードによって動作が異なる場合があります。次の表は、各修飾子によって許可されるメンバーへのアクセスを示しています。[ 25 ]
JavaScript には単純なスコープ規則がありますが、[ 26 ]変数の初期化と名前解決規則が問題を引き起こす可能性があり、コールバックにクロージャが広く使用されているため、関数が定義されたときの字句コンテキスト (名前解決に使用) は、関数が呼び出されたときの字句コンテキスト (名前解決には関係ない) とは大きく異なる可能性があります。JavaScript オブジェクトにはプロパティの名前解決がありますが、これは別のトピックです。
JavaScript には、関数レベルでネストされたレキシカル スコープ[ 27 ]があり、グローバル コンテキストが最も外側のコンテキストです。このスコープは、変数と関数 (関数型の変数ではなく、関数宣言を意味します) の両方に使用されます。[ 28 ]letおよびキーワードを使用したブロック スコープは、 ECMAScriptconst 6以降標準となっています。ブロック スコープは、ブロック全体を関数でラップしてから実行することで生成できます。これは、即時実行関数式(IIFE) パターンとして知られています。
JavaScriptのスコープは、字句レベル、関数レベルと単純ですが、関連する初期化と名前解決のルールは混乱を招くことがあります。まず、スコープ外の名前に代入すると、デフォルトではローカル変数ではなくグローバル変数が作成されます。次に、新しいローカル変数を作成するには、varキーワードを使用する必要があります。すると、変数は関数の先頭で値とともに作成され、undefined代入式に到達したときにその値が代入されます。
これは変数巻き上げ[ 30 ]として知られています。宣言は関数の先頭に巻き上げられますが、初期化は巻き上げられません。第三に、初期化前に変数にアクセスするとundefined、構文エラーではなく、 が返されます。第四に、関数宣言の場合、変数の初期化とは異なり、宣言と初期化の両方が関数の先頭に巻き上げられます。たとえば、次のコードは出力を含むダイアログを生成します。未定義ローカル変数の宣言はグローバル変数をシャドウイングするために巻き上げられますが、初期化は巻き上げられないため、使用時に変数は未定義になります。
a = 1 ; function f () { alert ( a ); var a = 2 ; } f ();さらに、JavaScript では関数は第一級オブジェクトであり、コールバックとして割り当てられたり、関数から返されたりすることが多いため、関数が実行されるときには、名前解決は、関数が最初に定義された場所 (定義のレキシカル コンテキスト) に依存し、呼び出されたレキシカル コンテキストや実行コンテキストには依存しません。JavaScript の特定の関数、特にコールバックとして使用されるクロージャのネストされたスコープ (最もグローバルなものから最もローカルなものまで) は、オブジェクトのプロトタイプ チェーンになぞらえて、スコープ チェーンと呼ばれることがあります。
JavaScriptでは、関数は第一級オブジェクトであるため、ネストされた関数を使用することでクロージャを作成できます。 [ 31 ]囲んでいる関数からネストされた関数を返すと、囲んでいる関数のローカル変数が返された関数の(非ローカルな)字句コンテキストとして含まれ、クロージャが生成されます。例:
function newCounter () { // 呼び出し時にインクリメントされるカウンター(0から開始)を返し、 // 新しい値を返すvar a = 0 ; var b = function () { a ++ ; return a ; }; return b ; } c = newCounter (); alert ( c () + ' ' + c ()); // "1 2" と出力されますJavaScript では、コールバックに使用されるため、クロージャが頻繁に使用されます。実際、ローカル コンテキストで関数をコールバックとしてフックしたり、関数から返したりすると、関数本体にバインドされていない変数がある場合、クロージャが作成されます (クロージャのコンテキストは、現在のレキシカル コンテキストのネストされたスコープ、つまり「スコープ チェーン」に基づいています)。これは意図しない場合があります。パラメータに基づいてコールバックを作成する場合、パラメータはクロージャに格納する必要があります。そうしないと、囲んでいるコンテキストの変数を参照するクロージャが意図せず作成され、変更される可能性があります。[ 32 ]
JavaScriptオブジェクトのプロパティの名前解決は、プロトタイプツリーにおける継承に基づいており、ツリー内のルートへのパスはプロトタイプチェーンと呼ばれます。これは、変数や関数の名前解決とは別個のものです。
Lispの方言には、スコープに関するさまざまな規則がある。
オリジナルのLispは動的スコープを使用していたが、ALGOLに影響を受けたSchemeがLispファミリーに静的(字句)スコープを導入した。
Maclisp は、デフォルトではインタプリタで動的スコープを、コンパイル済みコードではデフォルトで字句スコープを使用していたが、コンパイル済みコードはSPECIAL特定の変数の宣言を使用することで動的バインディングにアクセスできた。[ 33 ]しかし、Maclisp は字句バインディングを現代の言語で期待されるよりも最適化として扱っており、現代の Lisp の字句スコープで期待されるようなクロージャ*FUNCTION機能は備えていなかった。この問題を多少不器用に回避するために、別の操作 が利用可能だった。 [ 34 ]
Common LispはSchemeから字句スコープを採用した[ 35 ] 。Clojureも同様である。
ISLISP は、通常の変数に対して字句スコープを持っています。また、動的変数も持っていますが、それらは常に明示的にマークされています。defdynamic特別な形式で定義され、特別な形式で束縛されdynamic-let、明示的なdynamic特別な形式でアクセスされなければなりません。[ 36 ]
Emacs Lispのような Lisp の他のいくつかの方言は、デフォルトで動的スコープを使用しています。Emacs Lisp では、バッファごとにレキシカルスコープが利用できるようになりました。[ 37 ]
Python では、変数には関数スコープ、モジュールスコープ、グローバルスコープがあります。名前はスコープ (関数、モジュール、またはグローバルスコープ) の開始時にコンテキストに入り、ネストされていない関数が呼び出されるかスコープが終了するとコンテキストから抜けます。変数の初期化前に名前を使用すると、実行時例外が発生します。変数に単にアクセスする場合 (代入しない場合)、名前解決は LEGB (ローカル、囲み、グローバル、組み込み) ルールに従い、名前を最も狭い関連コンテキストに解決します。ただし、変数に代入する場合は、デフォルトでは、代入時ではなく、レベル (関数、モジュール、またはグローバル) の開始時にスコープが始まる変数が宣言されます。これらのルールはどちらも、使用前にglobalまたはnonlocal(Python 3 では) 宣言を行うことで上書きできます。これにより、マスクされた非ローカル変数がある場合でもグローバル変数にアクセスしたり、グローバル変数または非ローカル変数に代入したりすることができます。
簡単な例として、関数が変数をグローバルスコープに解決する場合を考えてみましょう。
def f () -> None : print ( x )x : str = "global" f () # 出力: globalはが呼び出されるx前に定義されているfため、 の定義内で参照された後に定義されているにもかかわらず、エラーは発生しませんf。これは字句的には前方参照であり、Python では許可されています。
ここで代入すると、新しいローカル変数が作成されますが、グローバル変数の値は変更されません。
def f () -> None : x : str = "f" print ( x )x : str = "global" print ( x ) # 出力: global f () # 出力: f print ( x ) # 出力: global関数内で変数に値を代入すると、その変数は関数内でローカル変数として宣言されます。したがって、その変数のスコープは関数全体となり、代入前にその変数を使用するとエラーが発生します。これは、ローカル変数のスコープが宣言された時点から始まるC言語とは異なります。以下のコードはエラーを発生させます。
def f () -> None : print ( x ) x : str = "f"x : str = "global" f () # Traceback (most recent call last): # File "<stdin>", line 1, in <module> # File "<stdin>", line 2, in f # UnboundLocalError: local variable 'x' referenced before assignmentglobalデフォルトの名前解決ルールは、または(Python 3 では) キーワードで上書きできますnonlocal。以下のコードでは、のglobal x宣言は、がグローバル変数に解決されるgことを意味しますx。したがって、(既に定義されているため) にアクセスでき、代入は新しいローカル変数を宣言するのではなく、グローバル変数に代入します。変数に代入しないため、デフォルトでグローバル変数に解決されるので、globalには宣言は不要であることに注意してください。f
def f () -> None : print ( x )def g () -> None : global x print ( x ) x = "g"x : str = "global" f () # 出力: global g () # 出力: global f () # 出力: gglobalネストされた関数にも使用できます。ネストされていない関数と同様にグローバル変数への代入を可能にするだけでなく、非ローカル変数が存在する場合にグローバル変数にアクセスするためにも使用できます。
def f () -> None : def g () -> None : global x print ( x ) x : str = "f" g ()x : str = "global" f () # 出力: globalネストされた関数の場合、nonlocal非ローカル変数に代入するための宣言も用意されており、これはglobalネストされていない関数で使用する場合と同様です。
def f () -> None : def g () -> None : nonlocal x # Python 3 のみx = "g" x : str = "f" g () print ( x )x : str = "global" f () # 出力: g print ( x ) # 出力: globalRは字句スコープ言語であり、自由変数の値がグローバル変数のセットによって決定される他のS実装とは異なり、R では関数が作成されたコンテキストによって決定されます。 [ 38 ]スコープ コンテキストには、プログラマが望む場合に動的スコープのエクスペリエンスをシミュレートできるさまざまな機能 (などparent.frame()) を使用してアクセスできます。
ブロックスコープはありません。
a <- 1 { a <- 2 } message ( a ) ## 2関数は、作成されたスコープにアクセスできます。
a <- 1 f <- function () { message ( a ) } f () ## 1関数内で作成または変更された変数は、その関数内に保持されます。
a <- 1 f <- function () { message ( a ) a <- 2 message ( a ) } f () ## 1 ## 2 message ( a ) ## 1関数内で作成または変更された変数は、明示的に外側のスコープへの代入が要求されない限り、その関数内に留まります。
a <- 1 f <- function () { message ( a ) a <<- 2 message ( a ) } f () ## 1 ## 2 message ( a ) ## 2Rはデフォルトでは字句スコープを持つが、関数スコープは変更可能である。
a <- 1 f <- function () { message ( a ) } my_env <- new.env () my_env $ a <- 2 f () ## 1 environment ( f ) <- my_env f () ## 2関数内で定義された変数は、関数のスコープ内でのみ定義されているため、関数外からはアクセスできません。ただし、関数は、自身が定義されているスコープ内で定義されているすべての変数と関数にアクセスできます。
バインドする変数が特殊として宣言されている場合、バインディングは、インタプリタが変数をバインドする方法を模倣するコードとしてコンパイルされます。
funarg 問題
」の解決に役立つことを意図しています
が、簡単なケースでのみ機能します。
*FUNCTION*FUNCTIONMacLisp は Lisp 1.5 の特殊変数の概念を改良しました... Common Lisp に最も影響を与えたのは、Lisp Machine Lisp、MacLisp、NIL、S-1 Lisp、Spice Lisp、および Scheme です。
別のメカニズム(つまり、、、および)によって確立され、アクセスさ
れ
ます
。
defdynamicdynamic-letdynamic
24にはオプションのレキシカルバインディングがあり、バッファごとに有効にできます。