Computer >> 컴퓨터 >  >> 프로그래밍 >> C#

C#의 ref, out, in 키워드 완벽 정리: 매개변수 전달 방식의 차이와 활용법

C#에서 대부분의 메서드는 메서드에 제공해야 할 데이터를 정의하는 매개변수(파라미터)를 0개 이상 가질 수 있습니다. 메서드를 호출하는 코드는 반드시 해당 데이터를 인자(argument) 형태로 메서드에 전달해야 합니다. 즉, 메서드는 자신의 입력값을 매개변수로 선언하고, 호출하는 쪽에서는 인자를 통해 값을 공급하는 구조입니다.

예를 들어 다음 메서드와 그 호출 코드를 살펴보겠습니다.

static void Greet(string greeting){
    Console.WriteLine(greeting);
}
...
Greet("Hello");

위 예제에서 greetingGreet() 메서드의 매개변수이고, "Hello"는 메서드에 전달되는 인자입니다.

값으로 전달(Pass by Value)의 기본 동작

메서드를 호출하며 인자를 전달할 때 기본적으로 값으로 전달(pass by value)됩니다. 이는 메서드로 전달되는 순간 값의 복사본이 생성된다는 의미입니다. 따라서 메서드 내부에서 인자에 가한 변경 사항은 원본 변수에는 전혀 반영되지 않습니다.

using System;
int number = 8;
Increase(number);
Console.WriteLine(number); // 출력: 8
static void Increase(int num){
    num = num + 1;
    Console.WriteLine(num); // 출력: 9
}

반면 객체처럼 참조 타입 변수를 전달할 때는 변수가 실제 객체가 아닌 참조(reference)를 담고 있기 때문에, C#은 여전히 참조 자체를 값으로 복사합니다. 하지만 복사된 참조 변수 역시 메모리상의 동일한 객체를 가리키므로, 메서드 내부에서 객체에 가한 변경 사항은 메서드 외부의 변수에도 그대로 나타납니다.

using System;
var john = new User{
    Name = "John",
    Salary = 50000
};
Promote(john);
Console.WriteLine(john.Salary); // 출력: 60000
static void Promote(User u){
    u.Salary += 10000;
    Console.WriteLine(u.Salary); // 출력: 60000
}

그러나 메서드 내부에서 변수 자체의 값을 변경하면 이야기가 다릅니다. 변경된 것은 어디까지나 복사본일 뿐 실제 객체가 아니므로, 그 변화는 메서드 밖에 반영되지 않습니다. 예를 들어 메서드 안에서 인자에 null을 할당해도 실제 변수는 그대로 유지됩니다.

using System;
var john = new User{
    Name = "John",
    Salary = 50000
};
Promote(john);
Console.WriteLine(john.Salary); // 출력: 50000
static void Promote(User u){
    u = null;
}

C#은 이러한 매개변수 전달 방식을 개발자가 직접 제어할 수 있도록 세 가지 한정자(modifier) 키워드를 제공합니다.

ref 한정자: 참조로 전달하기

C#에서는 ref 한정자를 사용해 매개변수를 참조로 전달할 수 있습니다. 전달되는 변수가 참조 타입이든 값 타입이든 관계없이, 매개변수와 인자는 동일한 메모리 위치를 가리키게 됩니다.

다음 예제에서 인자 u에 null을 설정하면 john 변수 역시 null이 되어 NullReferenceException이 발생합니다.

예제

using System;
var john = new User{
    Name = "John",
    Salary = 50000
};
Promote(ref john);
Console.WriteLine(john.Salary); // System.NullReferenceException 발생
static void Promote(ref User u){
    u = null;
}

주의: ref 한정자로 전달되는 인자는 반드시 전달하기 전에 초기화되어 있어야 합니다.

out 한정자: 함수 밖으로 값 꺼내기

out 한정자는 ref와 유사하지만 다음과 같은 중요한 차이가 있습니다.

  • 인자는 함수에 들어가기 전에 초기화될 필요가 없습니다.

  • 인자는 함수에서 빠져나오기 전에 반드시 초기화(할당)되어야 합니다.

아래 예제에서 Hire 함수는 out 한정자를 통해 전달받은 새 사용자 객체를 초기화합니다. 호출 시점에 john 변수가 즉석에서 선언되는 점도 주목할 만합니다.

using System;
Hire(out User john);
Console.WriteLine(john.Salary); // 출력: 50000
static void Hire(out User u){
    u = new User{
        Name = "John",
        Salary = 50000
    };
}

ref 한정자와 마찬가지로 out 한정자가 붙은 변수도 참조로 전달됩니다.

실무에서는 out 변수를 활용해 하나의 함수에서 여러 반환 값을 얻는 패턴이 자주 사용됩니다.

using System;
var john = new User{
    Name = "John",
    Salary = 50000
};
bool shouldPromote = Raise(john.Salary, out double hike);
Console.WriteLine(shouldPromote); // True
Console.WriteLine($"인상 금액 = {hike}"); // 출력: 5000
static bool Raise(int salary, out double hike){
    hike = salary * 0.1;
    return hike < 7000;
}

in 한정자: 읽기 전용 참조 전달

in 한정자는 ref, out 매개변수와 유사하지만, in 인자를 받는 메서드는 해당 인자의 값을 수정할 수 없다는 결정적인 차이가 있습니다. 수정을 시도하면 C# 컴파일러가 컴파일 타임 오류를 발생시킵니다.

in 매개변수는 컴파일러가 큰 크기의 값 타입 메모리를 매개변수 변수로 복사하는 비용을 줄여주는 동시에, 객체가 실수로 변경되는 것도 방지해 줍니다. 따라서 크기가 큰 값 타입(struct)을 메서드에 전달할 때 성능과 안전성 면에서 특히 유용합니다.

var point = new Coord();
Verify(point);
static void Verify(in Coord c){
    // 오류: 'in Coord'는 읽기 전용 변수이므로 할당할 수 없습니다.
    c = new Coord();
}
struct Coord{
    public int X;
    public int Y;
}

세 가지 한정자 비교 요약

한정자호출 전 초기화 필요 여부메서드 내부에서 값 변경
ref필요가능
out불필요필수 (반드시 할당해야 함)
in필요불가 (읽기 전용)

종합 예제

using System;
class Program{
    static void Main(){
        int number = 8;
        Increase(number);
        Console.WriteLine(number); // 출력: 8

        var john = new User { Name = "John", Salary = 50000 };
        Promote(john);
        Console.WriteLine(john.Salary); // 출력: 60000

        Leave(john);
        Console.WriteLine(john.Salary); // 출력: 60000

        LeaveByRef(ref john);
        Console.WriteLine(john?.Salary); // 아무것도 출력하지 않음

        User dave;
        Hire(out dave);
        Console.WriteLine(dave.Salary); // 출력: 50000
    }
    static void Increase(int num){
        num = num + 1;
        Console.WriteLine(num); // 출력: 9
    }
    static void Promote(User u){
        u.Salary += 10000;
        Console.WriteLine(u.Salary); // 출력: 60000
    }
    static void Leave(User u){
        u = null;
    }
    static void LeaveByRef(ref User u){
        u = null;
    }
    static void Hire(out User u){
        u = new User{
            Name = "John",
            Salary = 50000
        };  
    }
}
class User{
    public string Name { get; set; }
    public int Salary { get; set; }
}

실행 결과

9
8
60000
60000
60000
50000