C# - 클래스 안에 구조체를 포함하는 경우 발생하는 dynamic 키워드의 부작용
재미있는 질문입니다. ^^
dynamic 변수 할당은 도대체 어디에????
; https://www.sysnet.pe.kr/3/0/1076
문제의 코드를 단순화하면 다음과 같습니다.
public struct People
{
public int Age;
}
public class Casting
{
public People People;
}
static void Main(string[] args)
{
dynamic casting = new Casting();
casting.People.Age = 19;
Console.WriteLine(casting.People.Age); // 0이 출력됨
}
위의 상황에서 struct People 정의를 class People로 하면 정상적으로 출력이 됩니다. 그런 걸로 볼 때, 문제의 원인은 boxing/unboxing으로 인한 부작용이 맞는 것 같습니다.
문제를 좀 더 구체화시켜 볼까요?
static void Main(string[] args)
{
dynamic casting = new Casting();
People p1 = (People)casting.People;
People p2 = (People)casting.People;
p1.Age = 5;
p2.Age = 10;
Console.WriteLine(p1.Age); // 출력값: 5
Console.WriteLine(p2.Age); // 출력값: 10
}
보시는 것처럼, casting.People로 값을 받아올 때마다 별도의 struct 인스턴스가 생기는 것을 확인할 수 있습니다. 확실하게 Object ID를 다음과 같이 구해볼 수도 있습니다.
dynamic casting = new Casting();
System.Runtime.Serialization.ObjectIDGenerator
obj = new System.Runtime.Serialization.ObjectIDGenerator();
bool firstTime;
long id1 = obj.GetId(casting.People, out firstTime);
long id2 = obj.GetId(casting.People, out firstTime);
Console.WriteLine(id1); // 출력값: 1
Console.WriteLine(id2); // 출력값: 2
분명히, casting.People를 호출할 때마다 생성되는 인스턴스의 ObjectID 값이 다릅니다.
이에 대한 구체적인 원인을 파악하려면 .NET Reflector를 이용해보면 됩니다. 생성되는 코드를 단순화하기 위해 다음과 같이 예제 코드를 만들고 빌드한 다음,
static void Main(string[] args)
{
dynamic casting = new Casting();
People p1 = (People)casting.People;
}
.NET Reflector에서 확인해 보면 CallSite 코드 호출로 바뀌는 것을 볼 수 있습니다. CallSite는 public이기 때문에 우리가 직접 호출해도 무방한데요. 그래서 최종적으로 코드를 다음과 같이 바꾸는 것도 가능합니다.
static void Main(string[] args)
{
object casting = new Casting();
CallSite<Func<CallSite, object, People>> site1 = CallSite<Func<CallSite, object, People>>.Create(
Binder.Convert(CSharpBinderFlags.ConvertExplicit, typeof(People), typeof(Program)));
CallSite<Func<CallSite, object, object>> site2 = CallSite<Func<CallSite, object, object>>.Create(
Binder.GetMember(CSharpBinderFlags.None, "People", typeof(Program),
new CSharpArgumentInfo[] { CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.None, null) }));
People p1 = site1.Target.Invoke(site1, site2.Target.Invoke(site2, casting));
}
실제로 dynamic 키워드는 C# 컴파일러에 의해서 위와 유사하게 변경이 됩니다.
복잡하긴 해도, 천천히 뜯어보면 이해못할 것도 없습니다. ^^
우선, site1.Target.Invoke 먼저 살펴볼까요? CallSite.Target 을 .NET Reflector로 살펴보면,
public class CallSite<T> : CallSite where T: class
{
...[생략]...
[__DynamicallyInvokable]
public T Target;
}
T generic 개체를 반환합니다. 즉, CallSite<T>에 지정된 Func<CallSite, object, People> 인스턴스가 되는 것입니다. Func 개체는 .NET에서 delegate로 정의되어 있습니다.
[TypeForwardedFrom("System.Core, Version=3.5.0.0, Culture=Neutral, PublicKeyToken=b77a5c561934e089"), __DynamicallyInvokable]
public delegate TResult Func<in T1, in T2, out TResult>(T1 arg1, T2 arg2);
반환 타입이 TResult로 되어 있고, 이것은 Func 정의에서 3번째 Generic 타입으로 지정된 것입니다. 아하... 그럼 답이 나왔군요.
CallSite 정의에서 실제로 "People" 멤버값을 반환하는 것은 site2입니다. site2의 반환값은 아래와 같이 object로 지정되어 있습니다.
CallSite<Func<CallSite, object, object>> site2 = CallSite<Func<CallSite, object, object>>.Create(
Binder.GetMember(CSharpBinderFlags.None, "People", typeof(Program),
new CSharpArgumentInfo[] { CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.None, null) }));
따라서, site2.Target.Invoke의 결과값은 object가 되는 것입니다. 이 때문에, class 내부에 정의된 Value Type들은 모두 boxing이 되어 Heap에 복사되고 이후의 변경들은 모두 그 Heap에 복사된 개체를 대상으로 이루어지는 것과 마찬가지입니다.
정리해 보면, 다음의 코드는 결국,
dynamic casting = new Casting();
casting.People.Age = 19;
아래와 같이 코딩하는 것과 마찬가지 효과를 갖습니다.
Casting cast = new Casting();
cast.People.Age = 6;
object objPeople = cast.People; // boxing으로 인해 heap에 복사됨
People heapPeople = (People)objPeople; // heap에 있는 인스턴스가 unboxing 된 것임.
heapPeople.Age = 10;
Console.WriteLine(cast.People.Age); // 출력값: 6
Console.WriteLine(heapPeople.Age); // 출력값: 10
그런데, 왜? 다음의 결과는 제대로 동작하는지에 대해서 묻고 있습니다.
dynamic onePeople = new People();
onePeople.Age = 20;
Console.WriteLine(onePeople.Age);
아쉽게도 위의 것은 '제대로 동작하는 것처럼 보일 뿐' 실제로 의도했던 결과는 아닙니다. 예를 들어, 위의 코드를 풀어서 문제를 확인해 보면 다음과 같습니다.
Casting cast = new Casting();
cast.People.Age = 10;
dynamic onePeople = cast.People;
onePeople.Age = 15;
Console.WriteLine(cast.People.Age); // 출력값: 10
Console.WriteLine(onePeople.Age); // 출력값: 15
즉, onePeople.Age는 다시 boxing 되어 반환된 Heap 개체의 값을 다루는 것일 뿐, 원본 cast 개체에 담겨진 인스턴스를 대상으로 하는 것은 아닙니다.
사실, 문제가 되었던 코드도 위와 같은 식으로 변경해 주면 마찬가지로 보존된 값이 나옵니다.
dynamic casting = new Casting();
casting.People.Age = 19;
Console.WriteLine(casting.People.Age); // 0이 출력됨
==>
dynamic casting = new Casting();
People p1 = (People)casting.People;
p1.Age = 19;
Console.WriteLine(p1.Age); // 출력값: 19
[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]