메서드 매개 변수

기본적으로 C#의 인수는 값으로 함수에 전달됩니다. 이는 변수의 복사본이 메서드에 전달된다는 의미입니다. 값(struct) 형식의 경우 의 복사본이 메서드에 전달됩니다. 참조(class) 형식의 경우 참조의 복사본이 메서드에 전달됩니다. 매개 변수 한정자를 사용하면 참조로 인수를 전달할 수 있습니다. 다음 개념은 이러한 차이점과 매개 변수 한정자를 사용하는 방법을 이해하는 데 도움이 됩니다.

  • 값으로 전달은 메서드에 변수의 복사본을 전달한다는 의미입니다.
  • 참조로 전달은 메서드에 변수에 대한 액세스를 전달한다는 의미입니다.
  • 참조 형식 변수에는 해당 데이터에 대한 참조가 포함됩니다.
  • 값 형식 변수에는 해당 데이터가 직접 포함됩니다.

구조체는 값 형식(C# 참조)이므로 메서드에 구조체를 값으로 전달하는 경우 메서드가 구조체 인수의 복사본을 받아서 작동합니다. 메서드가 호출 메서드의 원래 구조체에 액세스할 수 없으므로 어떤 방식으로든 변경할 수 없습니다. 메서드는 복사본만 변경할 수 있습니다.

클래스 인스턴스는 값 형식이 아니라 참조 형식입니다. 메서드에 참조 형식을 값으로 전달하는 경우 메서드가 클래스 인스턴스에 대한 참조의 복사본을 받습니다. 두 변수 모두 동일한 개체를 참조하세요. 매개 변수는 참조의 복사본입니다. 호출된 메서드는 호출 메서드의 인스턴스를 다시 할당할 수 없습니다. 그러나 호출된 메서드는 참조 복사본을 사용하여 인스턴스 멤버에 액세스할 수 있습니다. 호출된 메서드가 인스턴스 멤버를 변경하는 경우 호출 메서드도 동일한 인스턴스를 참조하므로 해당 변경 내용을 확인합니다.

다음 예제의 출력에서 차이점을 보여 줍니다. 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-contextref-safe-context라는 두 가지 범위를 사용합니다.

  • safe-context는 모든 식에 안전하게 액세스할 수 있는 범위를 정의합니다.
  • ref-safe-context는 식에 대한 참조가 안전하게 액세스되거나 수정될 수 있는 범위를 정의합니다.

비공식적으로 이러한 범위를 코드가 더 이상 유효하지 않은 참조를 액세스하거나 수정하지 않도록 하는 메커니즘으로 생각할 수 있습니다. 참조는 유효한 개체 또는 구조체를 참조하는 한 유효합니다. safe-context는 변수가 할당되거나 다시 할당될 수 있는 시기를 정의합니다. ref-safe-context는 변수가 참조 할당됨 또는 참조 재할당됨될 수 있는 시기를 정의합니다. 할당은 변수를 새 값에 할당합니다. 참조 할당은 다른 스토리지 위치를 참조할 변수를 할당합니다.

참조 매개 변수

값 대신 참조로 인수를 전달하려면 매개 변수 선언에 다음 한정자 중 하나를 적용합니다.

  • ref: 메서드를 호출하기 전에 인수를 초기화해야 합니다. 메서드는 매개 변수에 새 값을 할당할 수 있지만 반드시 그렇게 할 필요는 없습니다.
  • out: 호출 메서드는 메서드를 호출하기 전에 인수를 초기화할 필요가 없습니다. 메서드는 매개 변수에 값을 할당해야 합니다.
  • readonly ref: 메서드를 호출하기 전에 인수를 초기화해야 합니다. 메서드는 매개 변수에 새 값을 할당할 수 없습니다.
  • in: 메서드를 호출하기 전에 인수를 초기화해야 합니다. 메서드는 매개 변수에 새 값을 할당할 수 없습니다. 컴파일러는 in 매개 변수에 대한 인수 복사본을 보관하기 위해 임시 변수를 만들 수 있습니다.

클래스의 멤버는 ref, ref readonly, in 또는 out만 다른 서명을 포함할 수 없습니다. 특정 형식의 두 멤버가 하나는 ref 매개 변수를 포함하고 다른 하나는 out, ref readonly 또는 in 매개 변수를 포함한다는 것 외에는 차이가 없으면 컴파일러 오류가 발생합니다. 그러나 다음 예제에 나와 있는 것처럼 메서드 하나에는 ref, ref readonly, in 또는 out 매개 변수가 포함되어 있고 다른 하나에는 값으로 전달되는 매개 변수가 포함되어 있으면 메서드를 오버로드할 수 있습니다. 숨기기나 재정의와 같이 서명이 일치해야 하는 다른 상황에서는 in, ref, ref readonlyout이 서명의 일부가 되며 서로 일치하지 않습니다.

매개 변수에 이전 한정자 중 하나가 있는 경우 해당 인수는 호환 가능한 한정자를 가질 수 있습니다.

  • ref 매개 변수의 인수에는 ref 한정자가 포함되어야 합니다.
  • out 매개 변수의 인수에는 out 한정자가 포함되어야 합니다.
  • in 매개 변수의 인수는 선택적으로 in 한정자를 포함할 수 있습니다. 대신 ref 한정자를 인수에 사용하면 컴파일러가 경고를 발급합니다.
  • ref readonly 매개 변수의 인수에는 in 또는 ref 한정자가 포함되어야 하지만 둘 다 포함되어서는 안 됩니다. 두 한정자가 모두 포함되어 있지 않으면 컴파일러는 경고를 발급합니다.

이러한 한정자를 사용하면 인수가 사용되는 방법을 설명합니다.

  • ref는 메서드가 인수 값을 읽거나 쓸 수 있음을 의미합니다.
  • out은 메서드가 인수 값을 설정함을 의미합니다.
  • ref readonly는 메서드가 읽기는 하지만 인수 값을 쓸 수 없음을 의미합니다. 인수는 참조로 전달되어야 합니다.
  • in는 메서드가 읽기는 하지만 인수 값을 쓸 수 없음을 의미합니다. 인수는 참조로 전달되거나 임시 변수를 통해 전달됩니다.

속성은 변수가 아닙니다. 이는 메서드이므로 ref 매개 변수에 전달할 수 없습니다. 다음 종류의 메서드에서는 이전 매개 변수 한정자를 사용할 수 없습니다.

  • async 한정자를 사용하여 정의하는 비동기 메서드
  • yield return 또는 yield break 문을 포함하는 반복기 메서드

확장 메서드에는 다음 인수 키워드 사용에 대한 제한 사항도 있습니다.

  • 확장 메서드의 첫 번째 인수에는 out 키워드를 사용할 수 없습니다.
  • 인수가 struct가 아니거나 구조체로 제한되지 않는 제네릭 형식인 경우 확장 메서드의 첫 번째 인수에 ref 키워드를 사용할 수 없습니다.
  • 첫 번째 인수가 struct가 아니면 ref readonlyin 키워드를 사용할 수 없습니다.
  • ref readonlyin 키워드는 구조체로 제한되는 경우에도 제네릭 형식에 사용할 수 없습니다.

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 한정자

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을 사용하는 경우 다음과 같은 두 가지 효과가 있습니다.

  • 호출 사이트에서 in을 지정하면 컴파일러가 일치하는 in 매개 변수로 정의된 메서드를 선택하게 됩니다. 그렇지 않으면 두 메서드가 in이 있을 때만 다른 경우 by 값 오버로드가 더 적합합니다.
  • in을 지정하면 인수를 참조로 전달하려는 의도를 선언하는 것입니다. in에 사용된 인수는 직접 참조할 수 있는 위치를 나타내야 합니다. outref 인수에는 동일한 일반 규칙이 적용됩니다. 상수, 일반 속성 또는 값을 생성하는 다른 식은 사용할 수 없습니다. 그렇지 않은 경우 호출 사이트에서 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`

이제 by 값 인수를 사용하는 다른 메서드를 사용할 수 있다고 가정하겠습니다. 결과는 다음 코드와 같이 변경됩니다.

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 한정자

메서드 선언에서 params 키워드 뒤에는 추가 매개 변수가 허용되지 않으며, params 키워드 하나만 메서드 선언에 사용할 수 있습니다.

params 매개 변수의 선언된 형식이 1차원 배열이 아닌 경우 컴파일러 오류 CS0225가 발생합니다.

params 매개 변수를 사용하여 메서드를 호출하면 다음을 전달할 수 있습니다.

  • 배열 요소 형식의 쉼표로 구분된 인수 목록입니다.
  • 지정된 형식의 인수 배열입니다.
  • 인수가 없습니다. 인수를 보내지 않는 경우 params 목록의 길이는 0입니다.

다음 예제에서는 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[]
*/