본문 바로가기

C#

C# 구조체, ref, out

반응형

C# 구조체 (Struct)

클래스처럼 사용자 정의 형식을 두는 법.

특징

1. new로 생성해도되고 안해도 된다.

2. 기본 생성자는 명시적으로 정의할 수 없다.

3. 매개변수를 갖는 생성자를 정의해도 디폴트생성자가 생성된다. (클래스는 생성x)

4. 매개변수를 받는 생성자의 경우 반드시 해당 코드내에서 구조체 모든 필드값을 할당해야한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
struct Vector
{
    public int X;
    public int Y;
    public Vector(int x, int y)
    {
        // 모든 필드 초기화 필수
        this.X = x;
        this.Y = y;
    }
    public override string ToString()
    {
        return "X: " + X + "Y : " + Y;
    }
}
class Program
{
      
    static void Main(string[] args)
    {
        Vector v1 = new Vector(); //new사용
        Vector v2; // new 없이도 가능
        v2.X = 0;
        Vector v3 = new Vector(510);
        Console.WriteLine(v3);
    }
}


[Vector 구조체 사용]

* int 같은 기본형도 new할당 해도되고 안해도 됨. 같은 해석.

1
2
int n; // n의 값은 0으로 갖고 있지만 개발자가 할당한 것은 아니다.
Console.WriteLine(n); // 컴파일 에러


구조체와 클래스가 아주 유사하지만 차이점은 값형식이냐 참조형식이냐 차이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
struct Vector
{
    public int X;
    public int Y;
    public Vector(int x, int y)
    {
        // 모든 필드 초기화 필수
        this.X = x;
        this.Y = y;
    }
    public override string ToString()
    {
        return "X: " + X + " Y : " + Y;
    }
}
class Point
{
    public int X;
    public int Y;
    public Point(int x, int y)
    {
        this.X = x;
        this.Y = y;
    }
    public override string ToString()
    {
        return "X: " + X + " Y : " + Y;
    }
}
class Program
{
      
    static void Main(string[] args)
    {
        Vector v1;
        v1.X = 5;
        v1.Y = 10;
        Vector v2 = v1; // 값 형식 대입
        v2.X = 7;
        v2.Y = 15;
 
        Console.WriteLine("v1 : " + v1);
        Console.WriteLine("v2 : " + v2);
 
        Point pt1 = new Point(6,12);
        Point pt2 = pt1;
 
        pt2.X = 7;
        pt2.Y = 22;
 
        Console.WriteLine("pt1 : " + pt1);
        Console.WriteLine("pt2 : " + pt2);
    }
}


결과 >

v1 : X: 5 Y : 10

v2 : X: 7 Y : 15

pt1 : X: 7 Y : 22

pt2 : X: 7 Y : 22

즉, 구조체는 스택영역에 값이 직접들어가서 깊은 복사가 되지만 클래스는 참조하는 주소가 복사되면서 스택영역의 변수가 같은 힙영역을 가리킨다.


클래스와 구조체를 선택하는 기준

1. 일반적으로 모든 사용자 정의타입은 클래스로 구현

2. 깊은/얕은 복사 차이가 민감한 타입은 선택적으로 구조체로 구현

3. 참조 형식은 가비지 콜렉터에 의해 관리받게 됨, 참조 형식을 쓸 때 가비지콜렉터에 부담이 되는데, 이런 부하를 피해야 할 때 구조체 선택.


ref / out 예약어

참조에 의한 호출을 지원하기 위한 예약어 ref와 out

값형식이든 참조형식이든 둘다 엄연히 스택에 있는 이 복사되는 상황이었다. (Call by value)

즉, 값형식은 스택영역메모리에 값이 직접 들어있어서 그것이 복사된것이고,

참조형식은 스택영역메모리에 힙영역의 주소가 들어있어서 그 주소가 복사되면서 같은 객체를 가리킨 것이다.

그러나 ref 예약어를 사용하면 그 스택영역메모리의 주소를 사용하면서 Call by reference 를 사용하게된다.

* ref를 구조체에서 사용하면 클래스처럼 "얕은 복사"로 전달한 것과 같은 효과를 낸다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
static void Main(string[] args)
{
    Vector v1;
    v1.X = 5;
    v1.Y = 10;
 
    Change(ref v1);
    Console.WriteLine(v1);
 
}
static void Change(ref Vector vt)
{
    vt.X = 7;
    vt.Y = 11;
}


결과 > x : 7 y : 11


참조 형식에 ref 예약어를 쓰면 어떨까?

참조 형식은 원래 얕은 복사가 되었지만 ref 예약어를 사용해서 쓰면

스택영역메모리의 값이 아닌스택영역메모리의 주소를 복사해가기 때문에 위의 Change와 유사한 함수에서 매개변수로 넘어간 객체가 변경될 수 있다.

* 주의할 점

메서드에 ref인자로 넘어가는 변수는 호출하는 측에서 반드시 값을 할당해야 한다.

1
2
3
4
5
6
7
int value1; // 값 할당 안되었으니 ref인자로 전달 불가
string text = null// null값이 할당 되었으니 ref인자로 전달 가능
int value2;
value2 = 5// 메서드 호출전에 할당되었으니 전달 가능
Vector vt;
vt.X = 5// X,Y가 포함된 벡터구조체 값에 Y가 할당되지 않았으므로 ref인자 전달 불가
Vector vt2 = new Vector() // X,Y필드가 0으로 초기화 되었으므로 ref인자 전달 




out 예약어와 ref의 차이점

out 예약어도 Call by reference를 가능케 하는 키워드다.

1. out으로 지정된 인자에 넘길 변수는 초기화되지 않아도 된다. 초기화해도 out인자를 받는 메서드에서 그값을 사용할 수 없음.

2. out으로 지정된 인자를 받는 메서드는 반드시 변수에 값을 넣어서 반환해야 한다.

즉, out은 ref의 기능 가운데 몇가지를 강제적으로 제한하면서 특별한 용도를 가짐.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
bool Divide(int n1, int n2, out int result)
{
    if(n2 == 0)
    {
        result = 0;
        return false;
    }
    result = n1/n2;
    return true;
}
 
int quotient;
if(Divide(15,3,out quotient) == true)
{
    Console.WriteLine(quotient); // 출력 : 5
}


[정수 두개를 받아 나눈 몫을 반환하는 메서드]

위와 같이 짜는 이유는 단순히 결과만 리턴하면 분모가 0일 경우 0을 리턴하지만 이게 분모에 0을 줘서 0을 리턴 받았는지 실제로 몫이 0이어서 0을 리턴했는지 알 수가 없기 때문이다.

결과값을 반드시 넣어서 리턴해야하는 약속을 지켰다.

만약 5번째줄의 result = 0; 에서 할당을 안했다면 리턴할 때 에러가 발생할 것이다.


이런 비슷한 용도로 TryParse라는 메서드가 있다.

닷넷프레임워크에서 각 기본타입에 TryParse라는 메서드를 제공하는데 이 메서드는 변환이 성공했는지 여부를 true / false 로 리턴하고 성공했으면 out으로 지정된 result 변수에 값을 반환한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
int n;
if(int.TryParse("123456"out n) == true)
{
    Console.WriteLine(n); // 결과 : 1234567    
}
 
double d;
if(double.TryParse("12E3"out d) == true)
{
    Console.WriteLine(d); // 결과 : 12000    
}
 
bool b;
if(bool.TryParse("true"out b) == true)
{
    Console.WriteLine(b); // 결과 : True    
}
 
short s;
if(short.TryParse("123456789"out n) == true// short 범위 초과=>false
{
    Console.WriteLine(n); // false로 인해 실행 안됨.    
}
int nn;
if(int.TryParse("정프로"out nn) == true// 숫자가 아니므로 false
{
    Console.WriteLine(nn); //false로 인해 실행 안됨.
}



반응형