[C#] 값에 의한 호출(Call by Value)과 참조에 의한 호출(Call by Reference)

Running boy·2023년 8월 6일
0

컴퓨터 공학

목록 보기
15/36

호출의 종류

값이 넘어가냐 참조가 넘어가냐의 개념이 아니다.

메모리의 관점에서 이해해야 한다.


값에 의한 호출(Call by Value)

변수의 스택 값이 복사되는 것을 의미한다.

일반적인 깊은 복사, 얕은 복사 모두 여기에 해당한다. (얕은 복사 역시 힙 주소를 저장한 스택 값을 복사하는 것이기 때문에 이에 해당한다.)


참조에 의한 호출(Call by Reference)

변수의 스택 값을 복사하지 않고 스택 값을 담고 있는 메모리 주소 자체를 넘기는 것을 의미한다.

대표적으로 ref, out, in 키워드가 있다.


ref

메서드의 인자로 주소 자체를 넘길 수 있는 키워드이다.

class Program
{
    static void Swap1(int x, int y)
    {
        int temp = x;
        x = y;
        y = temp;
    }

    static void Swap2(ref int x, ref int y)
    {
        int temp = x;
        x = y;
        y = temp;
    }

    static void Main(string[] args)
    {
        int x = 5;
        int y = 10;

        Swap1(x, y);
        Console.WriteLine($"x: {x}, y: {y}");

        Swap2(ref x, ref y);
        Console.WriteLine($"x: {x}, y: {y}");
    }
}

// 출력:
// x: 5, y: 10
// x: 10, y: 5

Swap1은 값 자체가 인자로 넘겨져서 메서드 외부에 영향을 주지 않는다.

하지만 Swap2는 값의 주소가 인자로 넘겨졌기 때문에 메서드 외부에 영향을 준다.


ref는 단순히 참조를 넘기는데에만 의의가 있을까?

ref가 참조에 의한 호출이라는 것을 생각하고 아래의 예시를 보자.

class Point
{
    public int X;
    public int Y;
}

class Program
{
    static void Main(string[] args)
    {
        Point pt1 = null;
        Change1(pt1);

        Console.WriteLine("pt1: " + pt1);
        Change2(ref pt1);

        Console.WriteLine("pt1: X = " + pt1.X + ", Y = " + pt1.Y);
    }

    private static void Change1(Point pt) // 얕은 복사
    {
        pt = new Point();
        pt.X = 6;
        pt.Y = 12;
    }

    private static void Change2(ref Point pt) // ref를 이용한 참조에 의한 호출
    {
        pt = new Point();
        pt.X = 7;
        pt.Y = 14;
    }
}

// 출력:
// pt1:
// pt1: X = 7, Y = 14

Change1은 얕은 복사이자 값에 의한 호출로서 인자를 넘겼다.

이 경우 스택에 pt라는 매개변수에 해당하는 메모리를 할당하고 pt1이 가리키는 참조 값을 복사한다.

즉 pt와 pt1은 동일한 참조를 가리킬 뿐 서로 다른 변수이다.

그렇기 때문에 이 상태에서 pt에 새로운 참조 값을 부여해도 외부의 pt1은 영향을 받지 않는다.


하지만 Change2는 참조에 의한 호출로서 인자를 넘겼다.

이 경우 매개변수 pt는 pt1의 메모리 주소를 그대로 넘겨받는다.

즉 pt와 pt1의 주소가 같으므로 둘은 같은 변수이다.

그렇기 때문에 이 상태에서 pt에 새로운 참조 값을 부여하면 주소가 같은 pt1에 고스란히 영향이 가게 된다.


이를 그림으로 표현하면 아래와 같다.

그림1

그림2


반환값과 지역 변수에서 ref의 사용

C# 7.0부터 메서드의 반환값과 지역 변수에 ref의 사용을 지원한다.

class Program
{
    class TestClass
    {
        public int n = 10;

        public int TestMethod1()
        {
            return n;
        }

        public ref int TestMethod2()
        {
            return ref n;
        }
    }

    static void Main(string[] args)
    {
        TestClass test = new TestClass();

        int n1 = test.TestMethod1();
        n1 = 5;
        
        Console.WriteLine(n1);
        Console.WriteLine(test.n);

        ref int n2 = ref test.TestMethod2();
        n2 = 5;

        Console.WriteLine(n2);
        Console.WriteLine(test.n);
    }
}

// 출력:
// 5
// 10
// 5
// 5

메서드 정의, 반환값, 메서드 사용, 반환값을 받을 지역 변수 무려 네 곳에서 ref 키워드를 명시해야 된다.

중요한 것은 ref만이 ref를 받을 수 있다는 것이다.

또한 아래와 같은 응용도 가능하다.

test.TestMethod2() = 10;
Console.WriteLine(test.n); // 10

int n = test.TestMethod2() = 20;
Console.WriteLine(n); // 20
Console.WriteLine(test.n); // 20

기존의 메서드는 "값"을 반환했던 것에 반해 ref 메서드는 "주소가 가리키는 메모리 영역"을 반환하기 때문에 이 자체를 값이자 변수로 사용할 수 있다.

단 ref를 반환값과 지역 변수로 사용할 때 두 가지 제약이 있다.
1. 메서드 내의 지역 변수를 return ref로 반환할 수 없다. 지역 변수의 라이프 사이클 범위 밖으로 값을 반환할 수 없기 때문이다.
2. ref 키워드를 지정한 변수는 다시 다른 변수를 가리킬 수 없다. (C# 7.3에서 해당 제약이 사라졌다.)


out

ref와 같은 참조에 의한 호출 키워드이다.

다만 out은 ref와 비교했을 때 몇가지 차이점이 있다.

ref vs out
1. ref는 인자를 넘기기 전에 반드시 초기화해야 되지만 out은 선언만 해도 된다. (설령 out을 초기화하고 넘겨줬다고 해도 메서드 내부에서 사용할 수 없다. 컴파일 에러가 발생한다.)
2. out으로 받은 매개변수는 메서드 내부에서 반드시 값을 할당해서 반환해야 한다.


선언만 해도 되는 특징 덕분에 아래와 같은 두가지 표현 모두 가능하다.

int n;
TestMethod(out n);
TestMethod(out int n);

in

ref와 같은 참조에 의한 호출 키워드이다.

다만 in은 ref에 readonly의 속성이 더해진 것이다.

struct TestStruct
{
    public int x;
    public int y;
}

class TestClass
{
    public void TestMethod(in TestStruct testStruct)
    {
        testStruct.x = 10; // 컴파일 에러 발생
    }
}

구조체와 같은 값 형식의 데이터를 인자로 주고받으면 깊은 복사가 발생하면서 메모리를 차지하게 된다.

이를 방지하고자 ref를 사용할 수 있으나 값이 변경되면서 오작동을 일으킬 위험이 있다.

메모리를 낭비하지 않으면서 값 형식의 데이터를 주고받고, 값의 변경 또한 예방하기를 원한다면 in 키워드를 사용하는 것이 현명하다.


ref/out/in

out, in은 모두 ref를 기반으로 만들어졌다.

그렇기 때문에 ref, out, in만으로 구분지어 메서드 오버로드를 할 수 없다.


참고 자료
시작하세요! C# 10 프로그래밍 - 정성태

profile
Runner's high를 목표로

0개의 댓글