本項では、識別子のスコープ(有効な範囲、異なるものに同じ名前を付けれない範囲)とオブジェクトの寿命(作ったオブジェクトがいつまで生きているか、ガベージ コレクションの対象外になっているか)について説明していきます。

※ 本項では、現時点までに説明していない概念がいくつか出てきます。現時点で説明済みのものは変数くらいなので、とりあえず変数が絡むところだけ読んで、残りは後々読み直してください。

目次

キーワード

概要

ローカル変数、メンバー名(メソッドなどの名前)、型名など、開発者が自由につけれる名前のことを識別子(identifier)と言います。「識別」(identify)の名のとおり、一意に区別するためにつける名前なので、基本的には複数のものに同じ名前は付けれません。

ただし、識別子には有効は範囲があります。この範囲を識別子のスコープ(scope)と言い、スコープ内では識別子名は一意でなければならず、逆に、スコープが違えば、別のものに同じ名前を付けることができます。

また、スコープと関連して、以下のようなものがあります。

  • スコープ: 別のものに同じ名前を付けられない範囲
    • 基本的には、その識別子を囲うブロック内がスコープです
  • 変数に格納したオブジェクトの寿命
    • 基本的に、変数のスコープを外れれば、そのオブジェクトは不要(GCの対象)になります
    • ただし、ラムダ式イテレーター非同期メソッドなど、オブジェクトの寿命を延ばしてしまう構文がいくつかあります
  • 変数を使える範囲:
    • スコープ内で、かつ、変数宣言より下でだけ変数を使えます
    • さらに、変数に格納した値を読み出すためには、確実に初期化してからでなければいけません

本稿では、これらについて説明して行きます。

識別子のスコープ

C#の識別子のスコープは、原則として、その識別子の定義場所を囲むブロック内です。例えば以下のようになります。

識別子のスコープ = 囲むブロック内

この範囲では、基本的に同じ名前は使えないということになります。

入れ子のブロック

スコープの範囲は、ブロックが入れ子になっている個所も含めます。 すなわち、以下のようなコードはコンパイル エラーになります。

public static void M()
{
    int x = 10;

    {
        int x = 20; // ここでエラー
        Console.WriteLine(x);
    }

    Console.WriteLine(x);
}

この例ではxという名前の変数が2つあります。1つ目のx(10を代入している方)のスコープはメソッドM全体になります。2つ目のx(20の方)のスコープはそれよりも1回り小さい内側のブロック内になりますが、この範囲は1つ目のxのスコープ内でもあります。 プログラミング言語によっては、この「入れ子のレベル違い」の同名識別子を認めているものもありますが、C#では認めません。 C#は、原則としてスコープ内で識別子の意味を変えない・上書かないという方針をとっています。

逆に、以下のようなコードであれば、2つのxがそれぞれ直近のブロック内だけをスコープにしているので、エラーにはなりません。

public static void M()
{
    {
        int x = 10;
        Console.WriteLine(x);
    }

    {
        // 別ブロック = 別スコープ。↑のxとは完全に別物
        string x = "a";
        Console.WriteLine(x);
    }
}

もう1つ注意が必要なのは、変数の定義位置がどこであろうと、スコープは直近のブロック全体になるということです。 例えば以下のコードを見てください。

public static void M3()
{
    {
        // 下で定義されている string の方の x と名前被り
        int x = 20; // コンパイル エラー
        Console.WriteLine(x);
    }

    // string の方の x はここから下でしか使えない
    // にも関わらず、x のスコープはメソッド内全体
    string x = "a";
    Console.WriteLine(x);
}

2つ目のx(stringの方)は下の方で定義されていますが、これのスコープはブロックの先頭からになります。 その結果、1つ目のxは「スコープ被り」で、同名が許されず、コンパイル エラーになります。

例外1: メンバーとローカル変数

「入れ子のもの含めて、スコープ内では同名不可」の原則には例外もあります。 1つは、以下のように、メンバーとローカル変数には同じ名前をつけれるということです。

public class Sample
{
    int x = 20;

    public void M()
    {
        int x = 10;

        Console.WriteLine(x);      // ローカル変数の方の x = 10
        Console.WriteLine(this.x); // フィールドの方の x = 20
    }
}

この場合、ローカル変数側が優先されます。フィールドの方を使うためにはthis.を付けるのが必須になります。

例外2: 型と名前空間

もう1つの例外は、型と名前空間です。外で定義された型の名前と同名のメンバーやローカル変数が作れます。

namespace Color
{
    public enum Color
    {
        Green,
        Yellow,
        Red,
    }

    public class Sample
    {
        public Color Color { get; set; }

        public void M()
        {
            Color Color = Color.Red;
        }
    }
}

この場合、どの識別子かを明確化するには、完全修飾名を使うことになります。

using System;

namespace Color
{
    public enum Color
    {
        Green,
        Yellow,
        Red,
    }

    public class Sample
    {
        public global::Color.Color Color { get; set; }

        public void M()
        {
            global::Color.Color Color = global::Color.Color.Red;

            Console.WriteLine(Color);
            Console.WriteLine(this.Color);
        }
    }
}

ちなみに、これは、あくまで型が外側のスコープで定義されている場合だけです。 以下のように、まったく同じスコープ内で定義する場合は、型名とメンバー名を同じにすることはできなくなります。

public class Sample
{
    public enum Color
    {
        Green,
        Yellow,
        Red,
    }

    // enum の Color と同じスコープ内でプロパティの Color を作ろうとしていて
    // この場合はコンパイル エラーになる
    public Color Color { get; set; }
}

引数

メソッドの引数のスコープは、そのメソッド本体内全域です。ほぼ、ローカル変数と扱いは一緒です。 メソッド内で、引数と同名のローカル変数は作れません。

public static void M(int x)
{
    int x = 10; // コンパイル エラー
    Console.WriteLine(x);
}

ローカル変数と同じくスコープの例外として、メンバーと同じ名前を付けることができます。 極端な話、以下のように、メソッドと同名の引数を使うこともできます。

public class Sample
{
    public static int X(int X)
    {
        if (X <= 1) return 1;
        else return Sample.X(X - 1);
    }
}

ループ変数

forステートメントや、foreachステートメントの場合、ループ変数があります。ループ変数のスコープはステートメントの内側になります。

for (int i = 0; i < 5; i++)
{
    // for の i のスコープはこのブロック内
    Console.WriteLine(i);
}

foreach (var i in Enumerable.Range(0, 5))
{
    // foreach の i のスコープはこのブロック内
    // for の方の i とは別物
    Console.WriteLine(i);
}

変数を使える範囲

変数を使える範囲は、スコープよりもやや厳しくなります。 前節の通り、スコープは、その識別子を囲うブロック全体になりますが、 変数の場合はそのブロック全体でから使えるわけではありません。

まず、変数は、変数宣言よりも前では使えません。

// 宣言より後なのでコンパイル エラー
x = 10;

int x; // 変数宣言

// 宣言より後なので OK
x = 20;

また、変数に格納された値を読み出すためには、それよりも前に確実に初期化を行っている必要があります。

{
    int x; // 未初期化変数

    // 初期化前には読めない。コンパイル エラー
    Console.WriteLine(x);
}

{
    int y; // 未初期化変数

    y = 10; // ここで初期化

    // これならOK
    Console.WriteLine(y);
}

C#では、変数が確実に初期化されているかどうかを結構真面目に判定しています。 例えば、以下のように、if ステートメントでは真偽両方で初期化されているかまで見ています。 (これを、「確実な代入ルール」(definite assignment rule)と呼んで、結構事細かにルールが決まっています。)

{
    int x; // 未初期化変数

    if (Console.ReadKey().Key == ConsoleKey.A)
    {
        x = 10;
    }

    // 条件を満たさない時に x が初期化されない。コンパイル エラー
    Console.WriteLine(x);
}

{
    int y; // 未初期化変数

    if (Console.ReadKey().Key == ConsoleKey.A)
    {
        y = 10;
    }
    else
    {
        y = 20;
    }

    // これならOK
    Console.WriteLine(y);
}

更新履歴

ブログ