메서드 매개변수와 한정자
메서드 매개변수와 한정자 (Method parameters and modifiers)
C#은 기본적으로 함수에 인자를 값으로 전달해요. 이 방식은 변수의 복사본을 메서드로 넘기는 거죠. 값(struct) 형식이라면 메서드가 그 값의 복사본을 받고, 참조(class) 형식이라면 참조의 복사본을 받습니다. 여기에 **매개변수 한정자(parameter modifier)**를 붙이면 인자를 참조로 전달할 수 있어요.
구조체(struct)는 값 형식이라 값을 넘기면 인자의 복사본이 메서드로 들어가요. 메서드는 그 복사본을 상대로 작업하죠. 때문에 호출한 쪽의 원본 구조체에는 접근도, 바꿀 수도 없고, 복사본만 바꿀 수 있어요.
클래스 인스턴스는 값 형식이 아니라 참조 형식입니다. 참조 형식을 값으로 넘기면 메서드는 인스턴스에 대한 참조의 복사본을 받아요. 그래서 두 변수는 같은 객체를 가리키죠. 매개변수는 참조의 복사본일 뿐이지만, 이 복사본으로 인스턴스 멤버에는 접근할 수 있어요. 메서드가 인스턴스 멤버를 바꾸면 호출한 쪽에서도 그 변경이 보입니다. 어차피 같은 인스턴스를 참조하고 있으니까요.
[!INCLUDEcsharp-version-note]
값 전달과 참조 전달 (Pass by value and pass by reference)
이 절의 모든 예제는 class 형식과 struct 형식의 차이를 보여주기 위해 다음 두 record 형식을 사용해요:
public record struct Point(int X, int Y);
// This doesn't use a primary constructor because the properties implemented for `record` types are
// readonly in record class types. That would prevent the mutations necessary for this example.
public record class Point3D
{
public int X { get; set; }
public int Y { get; set; }
public int Z { get; set; }
}
다음 예제의 출력은 구조체 형식을 값으로 넘길 때와 클래스 형식을 값으로 넘길 때의 차이를 보여줘요. 두 Mutate 메서드 모두 인자의 속성 값을 바꾸는데, 매개변수가 struct 형식이면 그 변경이 인자 데이터의 복사본에만 영향을 주고, class 형식이면 인자가 가리키는 인스턴스에 영향을 줘요.
public class PassTypesByValue
{
public static void Mutate(Point pt)
{
Console.WriteLine($"\tEnter {nameof(Mutate)}:\t\t{pt}");
pt.X = 19;
pt.Y = 23;
Console.WriteLine($"\tExit {nameof(Mutate)}:\t\t{pt}");
}
public static void Mutate(Point3D pt)
{
Console.WriteLine($"\tEnter {nameof(Mutate)}:\t\t{pt}");
pt.X = 19;
pt.Y = 23;
pt.Z = 42;
Console.WriteLine($"\tExit {nameof(Mutate)}:\t\t{pt}");
}
public static void TestPassTypesByValue()
{
Console.WriteLine("===== Value Types =====");
var ptStruct = new Point { X = 1, Y = 2 };
Console.WriteLine($"After initialization:\t\t{ptStruct}");
Mutate(ptStruct);
Console.WriteLine($"After called {nameof(Mutate)}:\t\t{ptStruct}");
Console.WriteLine("===== Reference Types =====");
var ptClass = new Point3D { X = 1, Y = 2, Z = 3 };
Console.WriteLine($"After initialization:\t\t{ptClass}");
Mutate(ptClass);
Console.WriteLine($"After called {nameof(Mutate)}:\t\t{ptClass}");
// Output:
// ===== Value Types =====
// After initialization: Point { X = 1, Y = 2 }
// Enter Mutate: Point { X = 1, Y = 2 }
// Exit Mutate: Point { X = 19, Y = 23 }
// After called Mutate: Point { X = 1, Y = 2 }
// ===== Reference Types =====
// After initialization: Point3D { X = 1, Y = 2, Z = 3 }
// Enter Mutate: Point3D { X = 1, Y = 2, Z = 3 }
// Exit Mutate: Point3D { X = 19, Y = 23, Z = 42 }
// After called Mutate: Point3D { X = 19, Y = 23, Z = 42 }
}
}
ref 한정자는 인자를 메서드에 참조로 전달하는 한 가지 방법이에요. 아래 코드는 앞선 예제를 그대로 두되 매개변수를 참조로 전달해요. 구조체를 참조로 넘기면 그 구조체에 가한 수정이 호출한 쪽에서 그대로 보이고, 참조 형식을 참조로 넘길 때는 의미상 변화가 없어요:
public class PassTypesByReference
{
public static void Mutate(ref Point pt)
{
Console.WriteLine($"\tEnter {nameof(Mutate)}:\t\t{pt}");
pt.X = 19;
pt.Y = 23;
Console.WriteLine($"\tExit {nameof(Mutate)}:\t\t{pt}");
}
public static void Mutate(ref Point3D pt)
{
Console.WriteLine($"\tEnter {nameof(Mutate)}:\t\t{pt}");
pt.X = 19;
pt.Y = 23;
pt.Z = 42;
Console.WriteLine($"\tExit {nameof(Mutate)}:\t\t{pt}");
}
public static void TestPassTypesByReference()
{
Console.WriteLine("===== Value Types =====");
var pStruct = new Point { X = 1, Y = 2 };
Console.WriteLine($"After initialization:\t\t{pStruct}");
Mutate(ref pStruct);
Console.WriteLine($"After called {nameof(Mutate)}:\t\t{pStruct}");
Console.WriteLine("===== Reference Types =====");
var pClass = new Point3D { X = 1, Y = 2, Z = 3 };
Console.WriteLine($"After initialization:\t\t{pClass}");
Mutate(ref pClass);
Console.WriteLine($"After called {nameof(Mutate)}:\t\t{pClass}");
// Output:
// ===== Value Types =====
// After initialization: Point { X = 1, Y = 2 }
// Enter Mutate: Point { X = 1, Y = 2 }
// Exit Mutate: Point { X = 19, Y = 23 }
// After called Mutate: Point { X = 19, Y = 23 }
// ===== Reference Types =====
// After initialization: Point3D { X = 1, Y = 2, Z = 3 }
// Enter Mutate: Point3D { X = 1, Y = 2, Z = 3 }
// Exit Mutate: Point3D { X = 19, Y = 23, Z = 42 }
// After called Mutate: Point3D { X = 19, Y = 23, Z = 42 }
}
}
지금까지는 매개변수의 속성을 바꿨죠. 메서드는 매개변수를 아예 새 값으로 **재할당(reassign)**할 수도 있어요. 재할당은 값 전달/참조 전달일 때 구조체와 클래스 형식에서 서로 다르게 동작해요. 다음 예제는 값으로 전달된 매개변수를 재할당할 때 구조체와 클래스 형식이 어떻게 행동하는지 보여줘요:
public class PassByValueReassignment
{
public static void Reassign(Point pt)
{
Console.WriteLine($"\tEnter {nameof(Reassign)}:\t\t{pt}");
pt = new Point { X = 13, Y = 29 };
Console.WriteLine($"\tExit {nameof(Reassign)}:\t\t{pt}");
}
public static void Reassign(Point3D pt)
{
Console.WriteLine($"\tEnter {nameof(Reassign)}:\t\t{pt}");
pt = new Point3D { X = 13, Y = 29, Z = -42 };
Console.WriteLine($"\tExit {nameof(Reassign)}:\t\t{pt}");
}
public static void TestPassByValueReassignment()
{
Console.WriteLine("===== Value Types =====");
var ptStruct = new Point { X = 1, Y = 2 };
Console.WriteLine($"After initialization:\t\t{ptStruct}");
Reassign(ptStruct);
Console.WriteLine($"After called {nameof(Reassign)}:\t\t{ptStruct}");
Console.WriteLine("===== Reference Types =====");
var ptClass = new Point3D { X = 1, Y = 2, Z = 3 };
Console.WriteLine($"After initialization:\t\t{ptClass}");
Reassign(ptClass);
Console.WriteLine($"After called {nameof(Reassign)}:\t\t{ptClass}");
// Output:
// ===== Value Types =====
// After initialization: Point { X = 1, Y = 2 }
// Enter Reassign: Point { X = 1, Y = 2 }
// Exit Reassign: Point { X = 13, Y = 29 }
// After called Reassign: Point { X = 1, Y = 2 }
// ===== Reference Types =====
// After initialization: Point3D { X = 1, Y = 2, Z = 3 }
// Enter Reassign: Point3D { X = 1, Y = 2, Z = 3 }
// Exit Reassign: Point3D { X = 13, Y = 29, Z = -42 }
// After called Reassign: Point3D { X = 1, Y = 2, Z = 3 }
}
}
앞선 예제에서 알 수 있듯, 매개변수를 새 값으로 재할당하면 그 변경은 호출한 쪽에서 보이지 않아요. 값 형식이든 참조 형식이든 마찬가지죠. 그럼 참조로 받은 매개변수를 재할당하면 어떨까요? 다음 예제가 그 동작을 보여줘요:
public class PassByReferenceReassignment
{
public static void Reassign(ref Point pt)
{
Console.WriteLine($"\tEnter {nameof(Reassign)}:\t\t{pt}");
pt = new Point { X = 13, Y = 29 };
Console.WriteLine($"\tExit {nameof(Reassign)}:\t\t{pt}");
}
public static void Reassign(ref Point3D pt)
{
Console.WriteLine($"\tEnter {nameof(Reassign)}:\t\t{pt}");
pt = new Point3D { X = 13, Y = 29, Z = -42 };
Console.WriteLine($"\tExit {nameof(Reassign)}:\t\t{pt}");
}
public static void TestPassByReferenceReassignment()
{
Console.WriteLine("===== Value Types =====");
var ptStruct = new Point { X = 1, Y = 2 };
Console.WriteLine($"After initialization:\t\t{ptStruct}");
Reassign(ref ptStruct);
Console.WriteLine($"After called {nameof(Reassign)}:\t\t{ptStruct}");
Console.WriteLine("===== Reference Types =====");
var ptClass = new Point3D { X = 1, Y = 2, Z = 3 };
Console.WriteLine($"After initialization:\t\t{ptClass}");
Reassign(ref ptClass);
Console.WriteLine($"After called {nameof(Reassign)}:\t\t{ptClass}");
// Output:
// ===== Value Types =====
// After initialization: Point { X = 1, Y = 2 }
// Enter Reassign: Point { X = 1, Y = 2 }
// Exit Reassign: Point { X = 13, Y = 29 }
// After called Reassign: Point { X = 13, Y = 29 }
// ===== Reference Types =====
// After initialization: Point3D { X = 1, Y = 2, Z = 3 }
// Enter Reassign: Point3D { X = 1, Y = 2, Z = 3 }
// Exit Reassign: Point3D { X = 13, Y = 29, Z = -42 }
// After called Reassign: Point3D { X = 13, Y = 29, Z = -42 }
}
}
이 예제에서 참조로 전달된 매개변수의 값을 재할당하면 그 변경이 호출한 쪽에도 보인다는 걸 확인할 수 있어요.
참조와 값의 안전 컨텍스트 (Safe context of references and values)
메서드는 매개변수의 값을 필드에 저장할 수 있어요. 매개변수를 값으로 전달하면 보통 안전합니다. 메서드가 값을 복사하고, 참조 형식은 필드에 저장할 때 여전히 도달 가능하니까요. 매개변수를 참조로 전달하는 건 컴파일러가 언제 참조를 새 변수에 할당해도 안전한지 정의해야 해서 까다로워요. 컴파일러는 모든 식에 대해 그 식이나 변수에 접근할 수 있는 범위를 제한하는 **안전 컨텍스트(safe context)**를 정의하고, 두 가지 범위를 사용해요: safe-context와 ref-safe-context.
- safe-context — 어떤 식이든 안전하게 접근할 수 있는 범위를 정의해요.
- ref-safe-context — 어떤 식에 대한 참조가 안전하게 접근·수정될 수 있는 범위를 정의해요.
비공식적으로는, 이 범위들이 코드가 더 이상 유효하지 않은 참조에 접근하거나 수정하지 않도록 보장하는 장치라고 생각하면 돼요. 참조는 유효한 객체나 구조체를 가리키는 한 유효하고, safe-context는 변수를 언제 할당·재할당할 수 있는지, ref-safe-context는 변수를 언제 ref 할당·ref 재할당할 수 있는지를 정의하죠. 할당은 변수에 새 값을 넣는 것이고, ref 할당은 변수가 다른 저장 위치를 가리키도록 만드는 거예요.
참조 매개변수 (Reference parameters)
값 대신 참조로 인자를 전달하려면 매개변수 선언에서 다음 한정자 중 하나를 사용해요:
ref: 메서드를 호출하기 전에 인자를 초기화해야 해요. 메서드가 매개변수에 새 값을 할당할 수는 있지만, 반드시 해야 하는 건 아니에요.out: 호출하는 쪽에서 호출 전에 인자를 초기화할 필요 없어요. 메서드는 매개변수에 값을 반드시 할당해야 해요.ref readonly: 메서드를 호출하기 전에 인자를 초기화해야 해요. 메서드는 매개변수에 새 값을 할당할 수 없어요.in: 메서드를 호출하기 전에 인자를 초기화해야 해요. 메서드는 매개변수에 새 값을 할당할 수 없어요. 컴파일러가in매개변수용으로 인자의 복사본을 담을 임시 변수를 만들 수도 있어요.
참조로 전달되는 매개변수는 **참조 변수(reference variable)**예요. 자기만의 값을 가지는 게 아니라, **피참조 변수(referent)**라고 하는 다른 변수를 가리키죠. 참조 변수는 ref 재할당이 가능해서, 가리키는 대상을 바꿀 수 있어요.
클래스의 멤버들은 ref, ref readonly, in, out만 다른 시그니처를 가질 수 없어요. 형식의 두 멤버가 오직 하나는 ref 매개변수, 다른 하나는 out·ref readonly·in 매개변수라는 차이만 있다면 컴파일러 오류가 발생하죠. 하지만 한 메서드가 ref·ref readonly·in·out 매개변수를 갖고 다른 메서드가 값으로 전달되는 매개변수를 갖는 경우에는 오버로드할 수 있어요(아래 예제 참고). 숨기기(hiding)나 재정의(overriding)처럼 시그니처 일치가 필요한 다른 상황에서는 in·ref·ref readonly·out이 시그니처의 일부라 서로 일치하지 않아요.
매개변수에 위 한정자 중 하나가 붙어 있으면, 그에 대응하는 인자도 호환되는 한정자를 가져야 해요:
ref매개변수의 인자는 반드시ref한정자를 포함해야 해요.out매개변수의 인자는 반드시out한정자를 포함해야 해요.in매개변수의 인자는 선택적으로in한정자를 포함할 수 있어요. 대신 인자에ref한정자를 쓰면 컴파일러가 경고를 내요.ref readonly매개변수의 인자는in이나ref한정자를 하나만 포함해야 해요 (둘 다는 안 돼요). 어떤 한정자도 포함하지 않으면 컴파일러가 경고를 내요.
이 한정자들은 인자가 사용되는 방식을 설명해요:
ref— 메서드가 인자 값을 읽거나 쓸 수 있다는 뜻.out— 메서드가 인자 값을 설정한다는 뜻.ref readonly— 메서드가 인자 값을 읽기만 하고 쓸 수 없다는 뜻. 인자는 참조로 전달되어야 해요.in— 메서드가 인자 값을 읽기만 하고 쓸 수 없다는 뜻. 인자는 참조로 전달되거나 임시 변수를 통해 전달돼요.
앞선 매개변수 한정자들은 다음 종류의 메서드에서는 쓸 수 없어요:
- async 한정자로 정의하는 비동기 메서드.
- yield return이나
yield break문을 포함하는 반복기 메서드.
확장 멤버도 이 인자 키워드 사용에 제약이 있어요:
out키워드는 확장 메서드의 첫 번째 인자에 쓸 수 없어요.ref키워드는 첫 번째 인자가struct가 아니거나 구조체로 제한되지 않은 제네릭 형식이 아니면 확장 메서드의 첫 번째 인자에 쓸 수 없어요.ref readonly와in키워드는 첫 번째 인자가struct일 때만 쓸 수 있어요.ref readonly와in키워드는 구조체로 제한됐더라도 어떤 제네릭 형식에도 쓸 수 없어요.
속성(property)은 변수가 아니라 메서드예요. 그래서 속성을 ref 매개변수의 인자로 쓸 수 없어요.
ref 매개변수 한정자 (ref parameter modifier)
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 parameter modifier)
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
암시적 형식 지역 변수(implicitly typed local variable)로 선언할 수도 있어요.
ref readonly 한정자 (ref readonly modifier)
메서드 선언에는 ref readonly 한정자가 필요하고, 호출 지점의 한정자는 선택적이에요. 호출 지점에서는 in이나 ref 한정자를 쓸 수 있는데 ref readonly는 쓸 수 없어요. 호출 지점에서 쓰는 한정자는 인자의 특성을 설명하는 데 도움을 줘요. ref는 인자가 변수이고 쓸 수 있을 때만, in은 인자가 변수일 때만 쓸 수 있어요 (쓸 수 있는 변수든 readonly 변수든 관계없어요). 인자가 변수가 아니라 식이라면 둘 다 붙일 수 없어요. 다음 예제들이 이런 조건들을 보여줘요. 아래 메서드는 성능 때문에 큰 구조체를 참조로 전달하라는 뜻으로 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 parameter modifier)
in 한정자는 메서드 선언에서는 필수지만 호출 지점에서는 필요 없어요.
var largeStruct = new LargeStruct { Value1 = 42, Value2 = 3.14, Value3 = "Hello" };
// Using 'in' avoids copying the large struct and prevents modification
ProcessLargeStruct(in largeStruct);
Console.WriteLine($"Original value unchanged: {largeStruct.Value1}");
// Without 'in', the struct would be copied (less efficient for large structs)
ProcessLargeStructByValue(largeStruct);
Console.WriteLine($"Original value still unchanged: {largeStruct.Value1}");
void ProcessLargeStruct(in LargeStruct data)
{
// Can read the values
Console.WriteLine($"Processing: {data.Value1}, {data.Value2}, {data.Value3}");
// Uncomment the following line to see error CS8331
// data.Value1 = 99; // Compilation error: cannot assign to 'in' parameter
}
void ProcessLargeStructByValue(LargeStruct data)
{
// This method receives a copy of the struct
Console.WriteLine($"Processing copy: {data.Value1}, {data.Value2}, {data.Value3}");
// Modifying the copy doesn't affect the original
data.Value1 = 99;
}
in 한정자는 컴파일러가 인자용 임시 변수를 만들어 그 인자에 대한 읽기 전용 참조를 전달하게 해줘요. 컴파일러는 인자를 변환해야 하거나, 인자 형식에서 암시적 변환이 있거나, 인자가 변수가 아닌 값(예: 리터럴 값, 속성 접근자가 반환한 값)일 때 항상 임시 변수를 만들어요. API가 인자를 참조로 전달해야 한다면 in보다는 ref readonly 한정자를 선택하세요.
in 매개변수로 메서드를 정의하면 성능 최적화를 얻을 수 있어요. 어떤 struct 형식 인자는 크기가 커서, 빡빡한 반복문이나 중요 코드 경로에서 메서드를 호출할 때 그 구조체를 복사하는 비용이 상당해요. in 매개변수로 선언하면 호출되는 메서드가 그 인자의 상태를 바꾸지 않으므로 인자를 안전하게 참조로 전달할 수 있어요. 참조로 넘기면 (잠재적으로 비싼) 복사가 사라지죠. 호출 지점에서 명시적으로 in 한정자를 붙이면 인자가 값이 아니라 참조로 전달되도록 보장할 수 있어요. 호출 지점에서 in을 명시적으로 쓰는 효과는 두 가지예요:
- 호출 지점에
in을 지정하면 컴파일러가 일치하는in매개변수로 정의된 메서드를 강제로 선택해요. 그렇지 않으면 두 메서드가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`
인자가 참조로 전달되는 유일한 호출은 마지막 것뿐이에요.
[!NOTE] 위 코드는 간단하게 하려고 인자 형식으로
int를 썼어요.int는 대부분의 현대 머신에서 참조보다 크지 않으므로,int하나를 읽기 전용 참조로 넘겨도 얻을 이득이 없어요.
params 한정자 (params modifier)
params 키워드가 붙은 매개변수는 메서드 선언에서 마지막 매개변수여야 하고, 한 메서드 선언에 params 키워드는 하나만 쓸 수 있어요.
params 매개변수는 컬렉션 형식으로 선언해야 해요. 인정되는 컬렉션 형식은 다음과 같아요:
- 1차원 배열 형식
T[](여기서 요소 형식은T). - 범위(span) 형식:
System.Span<T>System.ReadOnlySpan<T>이 형식들에서 요소 형식은T예요.
- 대응하는 요소 형식이 있는, 접근 가능한 create 메서드를 가진 형식. 그 create 메서드는 컬렉션 식과 같은 특성을 사용해요.
- xref:System.Collections.Generic.IEnumerable`1?displayProperty=fullName을 구현하는 구조체 형식이나 클래스 형식 (아래 조건 충족 시):
- 인자 없이 호출할 수 있는 생성자가 있고, 그 생성자가 선언하는 멤버 이상으로 접근 가능해야 해요.
- 단일 값 인자로 호출할 수 있는 인스턴스(확장이 아닌) 메서드
Add가 있고:- 메서드가 제네릭이라면 형식 인자를 인자에서 유추할 수 있어야 해요.
- 그 메서드는 선언 멤버 이상으로 접근 가능해야 해요. 이 경우 요소 형식은 그 형식의 반복(iteration) 형식이에요.
- 인터페이스 형식:
- xref:System.Collections.Generic.IEnumerable`1?displayProperty=fullName
- xref:System.Collections.Generic.IReadOnlyCollection`1?displayProperty=fullName
- xref:System.Collections.Generic.IReadOnlyList`1?displayProperty=fullName
- xref:System.Collections.Generic.ICollection`1?displayProperty=fullName
- xref:System.Collections.Generic.IList`1?displayProperty=fullName
이 형식들에서 요소 형식은
T예요.
C# 13 이전에는 매개변수에 1차원 배열을 사용해야 했어요.
params 매개변수가 있는 메서드를 호출할 때 넘길 수 있는 것은:
- 배열 요소 형식의 인자들을 쉼표로 구분한 목록.
- 지정된 형식의 컬렉션.
- 아무 인자도 없음. 인자를 보내지 않으면
params목록의 길이는 0이 돼요.
다음 예제는 params 매개변수에 인자를 보내는 여러 가지 방법을 보여줘요.
public static void ParamsModifierExample(params int[] list)
{
for (int i = 0; i < list.Length; i++)
{
System.Console.Write(list[i] + " ");
}
System.Console.WriteLine();
}
public static void ParamsModifierObjectExample(params object[] list)
{
for (int i = 0; i < list.Length; i++)
{
System.Console.Write(list[i] + " ");
}
System.Console.WriteLine();
}
public static void TryParamsCalls()
{
// You can send a comma-separated list of arguments of the
// specified type.
ParamsModifierExample(1, 2, 3, 4);
ParamsModifierObjectExample(1, 'a', "test");
// A params parameter accepts zero or more arguments.
// The following calling statement displays only a blank line.
ParamsModifierObjectExample();
// 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 };
ParamsModifierExample(myIntArray);
object[] myObjArray = { 2, 'b', "test", "again" };
ParamsModifierObjectExample(myObjArray);
// The following call causes a compiler error because the object
// array cannot be converted into an integer array.
//ParamsModifierExample(myObjArray);
// The following call does not cause an error, but the entire
// integer array becomes the first element of the params array.
ParamsModifierObjectExample(myIntArray);
}
/*
Output:
1 2 3 4
1 a test
5 6 7 8 9
2 b test again
System.Int32[]
*/
params 매개변수의 인자가 컬렉션 형식이면 오버로드 해석에서 모호함이 생길 수 있어요. 인자의 컬렉션 형식은 매개변수의 컬렉션 형식으로 변환 가능해야 해요. 서로 다른 오버로드가 그 매개변수에 더 나은 변환을 제공하면 그 메서드가 더 나을 수 있어요. 하지만 params 매개변수의 인자가 개별 요소이거나 없으면, 다른 params 매개변수 형식을 가진 모든 오버로드가 그 매개변수에 대해 동등해요.
더 알아보기
- 인자 목록(Argument lists) — C# 언어 사양의 해당 절. C# 구문과 사용법의 최종 출처는 언어 사양이에요.