대입하면 값이 복사되거나 주소가 복사된다
두 코드를 비교해 봅시다.
int x = 10;
int y = x;
y = 20;
Console.WriteLine(x); // 10
Dog a = new Dog("바둑");
Dog b = a;
b.Name = "초코";
Console.WriteLine(a.Name); // 초코
같은 모양인데 결과가 반대입니다. x 는 안 바뀌고 a.Name 은 바뀌었습니다.
규칙이 다른 것이 아닙니다. 규칙은 하나입니다. 담긴 것이 복사됩니다. 담긴 것이 무엇이냐가 다를 뿐입니다.
담긴 것이 값이면 값이 복사된다
int 는 값 타입입니다. 변수 안에 숫자가 그대로 들어 있습니다.
y = x 는 그 숫자를 옮겨 적습니다. 자리가 둘이 됩니다.
담긴 것이 주소면 주소가 복사된다
Dog 는 참조 타입입니다. 변수 안에 든 것은 개가 아니라 개가 있는 곳의 주소입니다.
b = a 는 그 주소를 옮겨 적습니다.
주소를 복사했으니 둘이 같은 곳을 가리킵니다. 개는 여전히 한 마리입니다.
마지막 단계가 헷갈리는 자리입니다. 두 가지를 구분해야 합니다.
| 하는 일 | 무엇이 바뀌나 | 다른 변수에 보이나 |
|---|---|---|
b.Name = "초코" |
가리키는 대상의 내용 | 보인다 |
b = new Dog(...) |
b 가 가리키는 곳 |
안 보인다 |
앞에서 메서드에 List 를 넘기면 밖이 바뀐다고만 하고 넘어간 자리가 있습니다.
이유가 이것입니다. 복사된 것이 목록이 아니라 주소였습니다.
list.Add(99) 는 가리키는 대상을 고친 것이라 밖에 보이고,
list = new List<int>() 는 가리키는 곳을 바꾼 것이라 안 보입니다.
어떤 것이 값이고 어떤 것이 참조인가
| 값 타입 | 참조 타입 | |
|---|---|---|
| 무엇이 있나 | int, double, bool, char, enum, struct |
class, 배열, string, List 같은 것들 |
| 변수에 담긴 것 | 값 자체 | 주소 |
| 어디에 놓이나 | 보통 스택 | 힙 (변수는 스택) |
| 대입하면 | 값이 복사된다 | 주소가 복사된다 |
null 이 될 수 있나 |
안 된다 | 된다 |
string 이 참조 타입이라는 것이 걸릴 수 있습니다.
앞에서 문자열은 바꿀 수 없다고 했는데, 그래서 값처럼 행동합니다.
string s1 = "안녕";
string s2 = s1;
s2 = "잘가"; // s2 가 다른 문자열을 가리키게 된 것
Console.WriteLine(s1); // 안녕
주소가 복사되긴 했는데, 가리키는 대상을 고칠 방법이 없으니 문제가 안 생깁니다. 문자열을 불변으로 만든 이유 중 하나가 이것입니다.
struct 는 값 타입이다
클래스와 거의 같게 생겼는데 값 타입인 것이 있습니다. struct 입니다.
struct Point
{
public int X;
public int Y;
}
Point p1 = new Point { X = 1, Y = 2 };
Point p2 = p1;
p2.X = 99;
Console.WriteLine(p1.X); // 1, 안 바뀝니다
class 를 struct 로 바꾸기만 했는데 결과가 반대가 됐습니다.
struct 는 변수 안에 내용이 통째로 들어 있어서, 대입하면 내용 전체가 복사됩니다.
class |
struct |
|
|---|---|---|
| 타입 | 참조 | 값 |
| 대입하면 | 주소가 복사 | 내용이 통째로 복사 |
null |
된다 | 안 된다 |
| 상속 | 된다 | 안 된다 |
언제 struct 를 쓰나. 작고, 값처럼 다루는 것이 자연스러운 묶음일 때입니다.
좌표, 색상, 크기 같은 것들입니다. (3, 4) 라는 좌표는 그 자체가 값이지
“어떤 좌표 객체” 가 아닙니다.
크면 쓰면 안 됩니다. 대입할 때마다 통째로 복사되니 필드가 많으면 그게 부담입니다.
애매하면 class 를 씁니다. 실무에서 struct 를 직접 만들 일은 드뭅니다.
박싱은 값을 힙으로 옮겨 담는 것이다
값 타입을 참조 타입 자리에 넣으면 문제가 생깁니다. 값은 스택에 있는데 참조 타입 자리는 주소를 기대하기 때문입니다.
C# 은 이때 힙에 상자를 하나 만들고 값을 거기 넣습니다. 이걸 박싱이라고 합니다.
int n = 42;
object o = n; // 박싱: 힙에 상자를 만들어 42 를 넣는다
int m = (int)o; // 언박싱: 다시 꺼낸다
object 는 모든 타입의 부모입니다. 무엇이든 받을 수 있는 자리인데,
참조 타입이라 값을 그대로는 못 담습니다.
문제는 비용입니다. 박싱은 new 를 부르는 것과 같습니다.
힙에 자리를 잡고, 나중에 GC 가 치웁니다.
for (int i = 0; i < 100000; i++)
{
object o = i; // 10만 번 박싱, 10만 개의 쓰레기
}
반복문 안에서 일어나면 눈에 띄게 느려집니다.
이 문제를 해결하는 것이 제네릭인데, 그건 뒤에서 다룹니다.
List<int> 의 꺾쇠가 바로 그것입니다.
정리
- 대입할 때 복사되는 것은 변수에 담긴 것입니다. 규칙은 하나입니다.
- 값 타입은 값이 담겨 있어서 값이 복사됩니다. 자리가 둘이 됩니다.
- 참조 타입은 주소가 담겨 있어서 주소가 복사됩니다. 둘이 같은 것을 가리킵니다.
- 가리키는 대상의 내용을 바꾸면 다른 변수에도 보입니다.
- 변수가 가리키는 곳을 바꾸면 자기만 바뀝니다.
string은 참조 타입이지만 고칠 수 없어서 값처럼 행동합니다.struct는 값 타입입니다. 작고 값처럼 다룰 묶음에만 씁니다. 애매하면class입니다.- 값 타입을
object자리에 넣으면 박싱이 일어납니다. 힙에 상자가 생기는 일입니다.