メソッド パラメーター

既定では、C# の引数は値渡しで関数に渡されます。 つまり、変数のコピーがメソッドに渡されます。 値 (struct) の型の場合、値のコピーがメソッドに渡されます。 参照 (class) 型の場合、参照のコピーがメソッドに渡されます。 パラメーター修飾子を使用すると、引数を参照渡しできます。 次の概念は、これらの区別とパラメーター修飾子の使用方法を理解するのに役立ちます。

  • "値渡し" は、メソッドに変数のコピーを渡すことを意味します。
  • "参照渡し" は、メソッドに変数へのアクセスを渡すことを意味します。
  • "参照型" の変数には、そのデータへの参照が格納されます。
  • "値の型" の変数には、そのデータが直接格納されます。

構造体は値の型であるため、メソッドに構造体が値渡しで渡されると、メソッドは構造体引数のコピーを受け取って操作します。 メソッドは、呼び出し側メソッドの元の構造体にはアクセスできないため、どのような場合でもこの構造体を変更することはできません。 メソッドで変更できるのはコピーのみです。

クラス インスタンスは、値の型ではなく、参照型です。 メソッドに参照型が値によって渡されると、メソッドはクラス インスタンスへの参照のコピーを受け取ります。 どちらの変数も同じオブジェクトを参照します。 パラメーターは参照のコピーです。 呼び出されたメソッドは、呼び出し元メソッド内のインスタンスを再割り当てできません。 ただし、呼び出されたメソッドは、参照のコピーを使用してインスタンス メンバーにアクセスできます。 呼び出されたメソッドがインスタンス メンバーを変更した場合、呼び出し元メソッドは、同じインスタンスを参照しているため、それらの変更を認識します。

次の例の出力はこの違いを示しています。 メソッド ClassTaker は、パラメーターのアドレスを使用してクラス インスタンスの指定されたフィールドを見つけるため、willIChange フィールドの値を変更します。 引数の値は構造体自体のコピーであり、アドレスのコピーではないため、呼び出し元メソッドの構造体の willIChange フィールドは StructTaker の呼び出しでは変更されません。 StructTaker はコピーを変更し、そのコピーは、StructTaker の呼び出しが完了したときに失われます。

class TheClass
{
    public string? willIChange;
}

struct TheStruct
{
    public string willIChange;
}

class TestClassAndStruct
{
    static void ClassTaker(TheClass c)
    {
        c.willIChange = "Changed";
    }

    static void StructTaker(TheStruct s)
    {
        s.willIChange = "Changed";
    }

    public static void Main()
    {
        TheClass testClass = new TheClass();
        TheStruct testStruct = new TheStruct();

        testClass.willIChange = "Not Changed";
        testStruct.willIChange = "Not Changed";

        ClassTaker(testClass);
        StructTaker(testStruct);

        Console.WriteLine("Class field = {0}", testClass.willIChange);
        Console.WriteLine("Struct field = {0}", testStruct.willIChange);
    }
}
/* Output:
    Class field = Changed
    Struct field = Not Changed
*/

パラメーター型と引数モードの組み合わせ

引数の受け渡し方法、および引数が参照型か値の型かによって、引数に加えられるどの変更が呼び出し元から見えるかが制御されます。

  • "値" の型を "値渡し" で渡す場合:
    • メソッドがパラメーターを代入して別のオブジェクトを参照する場合、それらの変更は呼び出し元からは見えません
    • メソッドがパラメーターによって参照されるオブジェクトの状態を変更する場合、それらの変更は呼び出し元からは見えません
  • "参照" 型を "値渡し" で渡す場合:
    • メソッドがパラメーターを代入して別のオブジェクトを参照する場合、それらの変更は呼び出し元からは見えません
    • メソッドがパラメーターによって参照されるオブジェクトの状態を変更する場合、それらの変更は呼び出し元から見えます
  • "値" の型を "参照渡し" で渡す場合:
    • メソッドがパラメーターを代入して別のオブジェクトを参照する場合、それらの変更は呼び出し元からは見えません
    • メソッドがパラメーターによって参照されるオブジェクトの状態を変更する場合、それらの変更は呼び出し元から見えます
  • "参照" 型を "参照渡し" で渡す場合:
    • メソッドがパラメーターを代入して別のオブジェクトを参照する場合、それらの変更は呼び出し元から見えます
    • メソッドがパラメーターによって参照されるオブジェクトの状態を変更する場合、それらの変更は呼び出し元から見えます

参照型を参照渡しで渡すと、呼び出されたメソッドは、参照パラメーターが呼び出し元で参照するオブジェクトに置換できます。 オブジェクトの格納場所は、参照パラメーターの値としてメソッドに渡されます。 パラメーターの格納場所の値を変更する場合は (新しいオブジェクトをポイント)、呼び出し元が参照する格納場所を変更することもできます。 次の例では、参照型のインスタンスを ref パラメーターとして渡します。

class Product
{
    public Product(string name, int newID)
    {
        ItemName = name;
        ItemID = newID;
    }

    public string ItemName { get; set; }
    public int ItemID { get; set; }
}

private static void ChangeByReference(ref Product itemRef)
{
    // Change the address that is stored in the itemRef parameter.
    itemRef = new Product("Stapler", 12345);
}

private static void ModifyProductsByReference()
{
    // Declare an instance of Product and display its initial values.
    Product item = new Product("Fasteners", 54321);
    System.Console.WriteLine("Original values in Main.  Name: {0}, ID: {1}\n",
        item.ItemName, item.ItemID);

    // Pass the product instance to ChangeByReference.
    ChangeByReference(ref item);
    System.Console.WriteLine("Calling method.  Name: {0}, ID: {1}\n",
        item.ItemName, item.ItemID);
}

// This method displays the following output:
// Original values in Main.  Name: Fasteners, ID: 54321
// Calling method.  Name: Stapler, ID: 12345

参照と値のセーフ コンテキスト

メソッドでは、パラメーターの値をフィールドに格納できます。 パラメーターが値渡しで渡される場合は、通常は安全です。 値がコピーされ、フィールドに格納された場合に参照型にアクセスできます。 パラメーターを参照渡しで安全に渡すには、コンパイラが新しい変数に参照を安全に代入するタイミングを定義する必要があります。 すべての式について、コンパイラは式または変数へのアクセスを制限する "セーフ コンテキスト" を定義します。 コンパイラは、safe-context と ref-safe-context という 2 つのスコープを使用します。

  • safe-context は、任意の式に安全にアクセスできるスコープを定義します。
  • ref-safe-context は、任意の式への "参照" に安全にアクセスしたり、変更したりできるスコープを定義します。

一般にこれらのスコープは、有効ではなくなった参照にコードがアクセスしたり、変更したりしないようにするためのメカニズムと考えることができます。 ある参照は、有効なオブジェクトまたは構造体を参照している限り有効です。 safe-context は、変数を代入または再代入できるタイミングを定義します。 ref-safe-context は、変数を ref 代入または ref 再代入できるタイミングを定義します。 代入により、変数が新しい値に割り当てられます。"ref 代入" では、変数を割り当てて別のストレージの場所を "参照" します。

参照パラメーター

パラメーター宣言に次のいずれかの修飾子を適用して、値渡しではなく参照渡しで引数を渡します。

  • ref: 引数は、メソッドを呼び出す前に初期化する必要があります。 メソッドはパラメーターに新しい値を割り当てることができますが、そうする必要はありません。
  • out: 呼び出し元メソッドは、メソッドを呼び出す前に引数を初期化する必要はありません。 メソッドはパラメーターに値を割り当てる必要があります。
  • readonly ref: 引数は、メソッドを呼び出す前に初期化する必要があります。 メソッドはパラメーターに新しい値を割り当てることはできません。
  • in: 引数は、メソッドを呼び出す前に初期化する必要があります。 メソッドはパラメーターに新しい値を割り当てることはできません。 コンパイラは、in パラメーターへの引数のコピーを保持するための一時変数を作成する場合があります。

クラスのメンバーは、refref readonlyinout のみが異なるシグネチャを持つことはできません。 1 つの型の 2 つのメンバー間の唯一の違いが、1 つには ref パラメーターが存在し、もう 1 つには outref readonlyin パラメーターが存在することである場合、コンパイラ エラーが発生します。 ただし、次の例に示すように、1 つのメソッドに refref readonlyinout パラメーターがあり、もう 1 つには値渡しされるパラメーターがある場合、メソッドをオーバーロードすることができます。 非表示やオーバーライドなど、シグネチャの一致が必要な他の状況では、inrefref readonlyout はシグネチャの一部であり、互いに一致しません。

パラメーターに前述のいずれかの修飾子がある場合、対応する引数には互換性のある修飾子を指定できます。

  • ref パラメーターの引数には、ref 修飾子を含める必要があります。
  • out パラメーターの引数には、out 修飾子を含める必要があります。
  • in パラメーターの引数には、必要に応じて in 修飾子を含めることができます。 代わりにその引数で ref 修飾子が使用されている場合、コンパイラは警告を出します。
  • ref readonly パラメーターの引数には、in 修飾子または ref 修飾子を含める必要がありますが、両方を含める必要はありません。 どちらの修飾子も含まれていない場合、コンパイラは警告を出します。

これらの修飾子を使用すると、次のように引数の使用方法が説明されます。

  • ref は、メソッドが引数の値を読み取ったり書き込んだりできることを意味します。
  • out は、メソッドが引数の値を設定することを意味します。
  • ref readonly は、メソッドが引数の値を読み取りますが、書き込めないことを意味します。 引数は参照渡しで渡す必要があります。
  • in は、メソッドが引数の値を読み取りますが、書き込めないことを意味します。 引数は、参照渡しまたは一時変数を介して渡されます。

プロパティは変数ではありません。 それらはメソッドであり、ref パラメーターに渡すことはできません。 次の種類のメソッドでは、前述のパラメーター修飾子を使用できません。

  • async 修飾子を使用して定義した Async メソッド。
  • yield return または yield break ステートメントを含む Iterator メソッド。

拡張メソッドには、これらの引数キーワードの使用に関する制限もあります。

  • 拡張メソッドの最初の引数では、out キーワードを使用できません。
  • 引数が struct でない場合、または構造体として制約されていないジェネリック型の場合、拡張メソッドの最初の引数で ref キーワードを使用することはできません。
  • 最初の引数が struct である場合を除き、ref readonly および in キーワードは使用できません。
  • ジェネリック型では、構造体として制約されている場合であっても、ref readonly および in キーワードを使用することはできません。

ref パラメーター修飾子

ref パラメーターを使用するには、メソッド定義と呼び出し元のメソッドの両方が、次の例に示すように ref キーワードを明示的に使用する必要があります。 (例外として、COM 呼び出しを行う場合は、呼び出し元のメソッドで ref を省略できます)。

void Method(ref int refArgument)
{
    refArgument = refArgument + 44;
}

int number = 1;
Method(ref number);
Console.WriteLine(number);
// Output: 45

ref パラメーターに渡す引数は、渡す前に初期化する必要があります。

out パラメーター修飾子

out パラメーターを使用するには、メソッド定義と呼び出し元のメソッドの両方で out キーワードを明示的に使用する必要があります。 次に例を示します。

int initializeInMethod;
OutArgExample(out initializeInMethod);
Console.WriteLine(initializeInMethod);     // value is now 44

void OutArgExample(out int number)
{
    number = 44;
}

out の引数として渡される変数は、メソッド呼び出しで渡される前に初期化する必要はありません。 ただし、呼び出されたメソッドでは、メソッドから制御が返される前に値を割り当てる必要があります。

Deconstruct メソッドは、複数の値を返すために、out 修飾子を使用してパラメーターを宣言します。 他のメソッドは、複数の戻り値の値タプルを返すことができます。

変数を別のステートメントで宣言してから、out 引数として渡すことができます。 out 変数を、別の変数宣言内ではなく、メソッド呼び出しの引数リスト内で宣言することもできます。 out 変数宣言により、よりコンパクトで読みやすいコードが生成されます。また、メソッド呼び出しの前に誤って変数に値を割り当てることもなくなります。 次の例は、Int32.TryParse メソッドの呼び出しで number 変数を定義しています。

string numberAsString = "1640";

if (Int32.TryParse(numberAsString, out int number))
    Console.WriteLine($"Converted '{numberAsString}' to {number}");
else
    Console.WriteLine($"Unable to convert '{numberAsString}'");
// The example displays the following output:
//       Converted '1640' to 1640

暗黙的に型指定されたローカル変数を宣言することもできます。

ref readonly modifier

ref readonly 修飾子は、メソッド宣言に存在する必要があります。 呼び出しサイトの修飾子は省略可能です。 in または ref 修飾子を使用できます。 ref readonly 修飾子は、呼び出しサイトでは無効です。 呼び出しサイトで使用する修飾子は、引数の特性を記述するのに役立ちます。 引数が変数であり、書き込み可能な場合にのみ、ref を使用できます。 引数が変数の場合にのみ in を使用できます。 書き込み可能な場合もあれば、読み取り専用の場合もあります。 引数が変数ではなく式である場合は、どちらの修飾子も追加できません。 次の例は、これらの条件を示しています。 次のメソッドでは、ref readonly 修飾子を使用して、パフォーマンス上の理由から、大きな構造体を参照渡しする必要があることを示しています。

public static void ForceByRef(ref readonly OptionStruct thing)
{
    // elided
}

ref または in 修飾子を使用してメソッドを呼び出すことができます。 修飾子を省略すると、コンパイラは警告を出します。 引数が変数ではなく式の場合は、in または ref 修飾子を追加できないため、警告を抑制する必要があります。

ForceByRef(in options);
ForceByRef(ref options);
ForceByRef(options); // Warning! variable should be passed with `ref` or `in`
ForceByRef(new OptionStruct()); // Warning, but an expression, so no variable to reference

変数が readonly 変数の場合は、in 修飾子を使用する必要があります。 代わりに ref 修飾子を使用すると、コンパイラはエラーを出します。

ref readonly 修飾子は、引数が変数ではない式ではなく、変数であるとメソッドは想定している、ということを示します。 変数ではない式の例としては、定数、メソッドの戻り値、プロパティがあります。 引数が変数でない場合、コンパイラは警告を出します。

in パラメーター修飾子

メソッド宣言では in 修飾子が必要ですが、呼び出しサイトでは不要です。

int readonlyArgument = 44;
InArgExample(readonlyArgument);
Console.WriteLine(readonlyArgument);     // value is still 44

void InArgExample(in int number)
{
    // Uncomment the following line to see error CS8331
    //number = 19;
}

in 修飾子を使用すると、コンパイラは引数の一時変数を作成し、その引数への読み取り専用参照を渡すことができます。 コンパイラは、引数を変換する必要がある場合、引数の型から暗黙的な変換がある場合、または引数が変数ではない値である場合は、常に一時変数を作成します。 たとえば、引数がリテラル値の場合や、プロパティ アクセサーから返される値の場合です。 API で引数を参照渡しする必要がある場合は、in 修飾子ではなく ref readonly 修飾子を選択します。

in パラメーターを使用して定義されたメソッドには、パフォーマンスを最適化できる可能性があります。 一部の struct 型引数は、サイズが大きくなる可能性があり、メソッドが短いループまたは重要なコード パスで呼び出される場合、その構造のコピーのコストが大きくなります。 呼び出されたメソッドは引数の状態を変更しないため、メソッドで in パラメーターを宣言して、参照渡しで安全に渡すことができる引数を指定します。 参照によりこれらの引数を渡して、(可能性がある) 高額なコピーを回避します。 呼び出しサイトで in 修飾子を明示的に追加して、引数が値ではなく、参照で渡されるようにします。 明示的に in を使用すると、次の 2 つの効果があります。

  • 呼び出しサイトで in を指定すると、コンパイラは、一致する in パラメーターで定義されたメソッドを強制的に選択します。 それ以外の場合は、2 つのメソッドで in の有無のみが異なるときは、値によるオーバーロードの方が適しています。
  • in を指定することで、参照渡しで引数を渡す意図を宣言します。 in で使用される引数では、直接参照できる場所を表す必要があります。 out および ref 引数と同じ一般ルールが適用されます。定数、通常のプロパティ、または値を生成するその他の式を使用することはできません。 それ以外の場合は、呼び出しサイトで in を省略すると、メソッドに読み取り専用の参照渡しで渡すために、一時変数を作成しても問題ないことが、コンパイラに通知されます。 コンパイラでは、一時変数を作成して、in 引数でのいくつかの制限に対処します。
    • 一時変数では、in パラメーターとしてコンパイル時の定数を許可します。
    • 一時変数では、プロパティ、または in パラメーターのその他の式を許可します。
    • 一時変数では、引数の型からパラメーターの型への暗黙の変換がある引数を許可します。

先のすべてのインスタンスで、コンパイラは定数、プロパティ、またはその他の式の値を格納する一時変数を作成します。

これらのルールが発生するコード例を次に示します。

static void Method(in int argument)
{
    // implementation removed
}

Method(5); // OK, temporary variable created.
Method(5L); // CS1503: no implicit conversion from long to int
short s = 0;
Method(s); // OK, temporary int created with the value 0
Method(in s); // CS1503: cannot convert from in short to in int
int i = 42;
Method(i); // passed by readonly reference
Method(in i); // passed by readonly reference, explicitly using `in`

ここでは、値渡しの引数を使用する別のメソッドが使用できたと想定します。 次のコードに示すように、結果が変更されます。

static void Method(int argument)
{
    // implementation removed
}

static void Method(in int argument)
{
    // implementation removed
}

Method(5); // Calls overload passed by value
Method(5L); // CS1503: no implicit conversion from long to int
short s = 0;
Method(s); // Calls overload passed by value.
Method(in s); // CS1503: cannot convert from in short to in int
int i = 42;
Method(i); // Calls overload passed by value
Method(in i); // passed by readonly reference, explicitly using `in`

引数が参照で渡されるメソッドの呼び出しは、最後のメソッドのみです。

注意

先のコードでは、わかりやすくするために int を引数型として使用します。 int は最新のマシンの参照より大きくなることはないため、読み取り専用の参照として単一の int を渡すメリットはありません。

params modifier

1 つのメソッド宣言内では、params キーワード以後に他のパラメーターを使用できず、1 つの params キーワードだけを使用できます。

params パラメーターの宣言された型が 1 次元配列でない場合は、コンパイラ エラー CS0225 が発生します。

params パラメーターを使用してメソッドを呼び出す場合は、以下を渡すことができます。

  • 配列要素の型の引数のコンマ区切りのリスト。
  • 指定した型の引数の配列。
  • 引数なし。 引数を渡さない場合、params リストの長さはゼロになります。

次の例に示すように、さまざまな方法で params パラメーターに引数を渡すことができます。

public class MyClass
{
    public static void UseParams(params int[] list)
    {
        for (int i = 0; i < list.Length; i++)
        {
            Console.Write(list[i] + " ");
        }
        Console.WriteLine();
    }

    public static void UseParams2(params object[] list)
    {
        for (int i = 0; i < list.Length; i++)
        {
            Console.Write(list[i] + " ");
        }
        Console.WriteLine();
    }

    static void Main()
    {
        // You can send a comma-separated list of arguments of the
        // specified type.
        UseParams(1, 2, 3, 4);
        UseParams2(1, 'a', "test");

        // A params parameter accepts zero or more arguments.
        // The following calling statement displays only a blank line.
        UseParams2();

        // An array argument can be passed, as long as the array
        // type matches the parameter type of the method being called.
        int[] myIntArray = { 5, 6, 7, 8, 9 };
        UseParams(myIntArray);

        object[] myObjArray = { 2, 'b', "test", "again" };
        UseParams2(myObjArray);

        // The following call causes a compiler error because the object
        // array cannot be converted into an integer array.
        //UseParams(myObjArray);

        // The following call does not cause an error, but the entire
        // integer array becomes the first element of the params array.
        UseParams2(myIntArray);
    }
}
/*
Output:
    1 2 3 4
    1 a test

    5 6 7 8 9
    2 b test again
    System.Int32[]
*/