[Unity] Struct (구조체)
Struct (구조체)?
- 데이터와 관련 기능을 캡슐화할 수 있는 값 형식
캡슐화 : 데이터와 기능을 하나로 묶고, 구현 내용 일부를 외부에 감춤
특징
1. 매개 변수가 없는 생성자를 선언할 수 없음 ( 단, C# 10부터 생성할 수 있게 됨 ). 해당 형식의 기본 값을 생성하는 매개 변수 없는 암시적 생성자를 제공하고 있음
기본 값 : struct (구조체)의 기본 값은 모든 값 형식 필드를 각 형식의 기본 값으로, 참조 필드를 null로 설정함
1) 매개 변수 없는 암시적 생성자를 제공하기 때문에 new struct()를 사용할 수 있음
2) 매개 변수 없는 명시적 생성자를 생성하려고할 경우 에러 발생
public struct A
{
// 구조체는 매개 변수 없는 명시적 생성자를 추가할 수 없기 때문에 컴파일 에러
public A() {}
}
3) 매개 변수 있는 명시적 생성자를 사용할 경우, struct 안에 있는 모든 필드의 할당이 필요하게 됨
public struct A
{
int _a;
int _b;
// _b가 할당되지 않았기 때문에 컴파일 에러
public A(int a)
{
_a = a;
}
}
// 모든 필드를 할당 할 경우 매개 변수 있는 생성자를 만들 수 있음
public struct A
{
int _a;
int _b;
public A(int a)
{
_a = a;
_b = 1;
}
public A(int a, int b)
{
_a = a;
_b = b;
}
}
2. struct ( 구조체 )의 인스턴스화
1) new 연산자를 이용해 적절한 생성자를 호출함으로써 인스턴스화하는 방법
2) struct의 모든 필드가 액세스가 가능할 경우 new 연산자 없이 인스터스화 할 수 있지만, 이 경우 인스턴스를 처음 사용하기 전 모든 필든 필드를 초기화해야함
public struct A
{
public int _a;
public void Test() { }
}
public class StructTest
{
void Start()
{
A a_new = new A();
a_new.Test();
A a_not_new;
// new로 인스턴스를 만들지 않는 경우, 구조체 안의 필드 값을 초기화하지 않으면 컴파일 에러가 발
a.Test();
A a_not_new_init;
a_not_new_init._a = 1; // 구조체 안의 필드를 할당했기 때문에 에러가 발생하지 않음
a_not_new_init.Test();
}
}
3. struct (구조체)는 상속을 할 수 없지만, interface 는 사용할 수 있음
public struct A
{
public int _a;
}
// B 구조체에 A를 상속하려고할 경우 컴파일 에러가 발생
public struct B : A { }
public struct A
{
// 상속이 없기 때문에 protected를 사용할 경우 컴파일 에러가 발생함
protected int _a;
}
public interface A
{
public void IAddOne();
}
// interface A는 사용할 수 있음
public struct B : A {
int _value;
public void IAddOne()
{
_value++;
}
}
* interface를 사용한 구조체를 사용할 경우, interface를 이용하여 함수를 호출하면 박싱으로 인해 의도치 않은 값이 나올 수 있음
public interface A {
public int value { set; get; }
public void IAddOne();
}
// interface A는 사용할 수 있음
public struct B : A {
public int value { get; set; }
public void IAddOne()
{
value++;
}
}
public class StructTest : MonoBehaviour
{
void Start()
{
B _b = new B();
_b.IAddOne();
// 초기값 value : 0에 value++를 했기 때문에 1이 나옴
Debug.Log(" _b Value : " + _b.value);
A _a = _b;
_a.IAddOne();
// _b.Value가 1이기 떄문에 IAddOne()을 하면 2 값이 출력될 것으로 예상했지만 값이 1이 나옴
// interface를 사용할 경우, 값 타입인 struct _b는 박싱되어 힙 메모리에 값이 복제 되기 떄문에
// _b 의 값이 변경되지 않는다.
Debug.Log(" a -> _b Value : " + _b.value);
// _a의 Value를 호출하면 값이 2가 된 것을 확인할 수 있다.
Debug.Log(" a Value : " + _a.value);
}
}
A a = (A) b
.locals init (
[0] valuetype B b,
[1] class A a
)
IL_002d: ldloc.0 // b
IL_002e: box B
IL_0033: stloc.1 // a
// a 에 valueType b가 box 되어 할당되는 것을 알 수 있음
// a 는 class로 정의됨
4. struct (구조체)는 System.ValueType을 상속하며, ValutType은 System.Object를 상속받았다. override 로 System.ValueType 에서 상속받은 메소드를 재정의할 수 있지만, 구조체 내의 함수는 abstract, virtual을 사용할 수 없다.
public struct A
{
// 구조체에서 abstarct 함수를 정의할 수 없기 때문에 컴파일 에러
public abstract void A1();
// 구조체에서 virtual 함수를 정의할 수 없기 때문에 컴파일 에러
public virtual void A2() { }
// System.ValueType의 함수는 재정의 할 수 있음
public override bool Equals(object obj) {
return base.Equals(obj);
}
public override int GetHashCode() {
return base.GetHashCode();
}
public override string ToString() {
return base.ToString();
}
}
5. struct (구조체)는 암시적으로 봉인되어 있기 때문에 abstract, sealed 를 사용할 수 없다.
// 구조체에 abstract를 사용할 수 없음, 컴파일 에러
public abstract struct A { }
// 구조체에 sealed를 사용할 수 없음, 컴파이 에러
public sealed struct B { }
6. struct (구조체)는 필드 선언에 변수 초기화를 할 수 없다.
public class B { }
public struct A
{
// 구조체는 필드 선언 시 변수 초기화를 할 수 없어 컴파일 에러
// 구조체가 생성될 때 필드의 선언된 값이 초기화 되기 때문에 값형식, 참조형식 모두 초기화할 수 없음
int _a = 1;
B _b = null;
}
7. struct (구조체)를 대입할 경우 값이 복사 된다.
- 구조체는 값이 복사 되기 때문에, 구조체가 커질 경우 값 복사의 오버헤드가 발생하게 됨
public struct A
{
public int _a;
}
public class StructTest : MonoBehaviour
{
void Start()
{
A a1 = new A();
a1._a = 1;
A a2 = a1;
a2._a = 2;
// a1 : 1 a2 : 2 가 출력됨
// 값이 복사 됬기 때문에 a1, a2는 다름
Debug.Log(" a1 : " + a1._a + " a2 : " + a2._a);
}
}
8. struct (구조체)는 소멸자를 선언할 수 없다.
public struct A
{
// 구조체는 소멸자를 정의할 수 없기 때문에 컴파일 에러
~A() {}
}
* struct (구조체) 가 stack/heap에 할당 되는 경우
- 아래 내용은 https://www.sysnet.pe.kr/2/0/12624를 참고하여 정리한 내용
1) class (클래스)에 정의된 struct (구조체)는 heap할당된다.
2) struct (구조체)에 정의된 참조형식의 경우 값 형식은 stack에 할당되고, 참조형은 heap에 할당된다.
3) 배열 (참조형)로 정의된 struct (구조체)는 heap에 할당된다.
* struct (구조체) vs class (클래스)의 선택
- 아래 내용은 https://docs.microsoft.com/ko-kr/dotnet/standard/design-guidelines/choosing-between-class-and-struct 을 참고하여 정리한 내용
클래스 (참조형식)과 구조체 (값 형식)으로 디자인을 결정하는 것에 있어 동작 방식의 차이를 이해하는 것이 중요
1) 참조 형식은 heap에 할당되고, 값 형식은 stack에 할당 된다. heap은 GC에 의해 메모리 할당 및 해제가 발생하고, stack은 크기가 미리 지정되어 있고 범위를 벗어나면 메모리가 해제되기 때문에 값 형식이 참조 형식보다 메모리 할당 및 해제가 빠르다.
2) 값 형식의 배열은 배열 요소가 실제 인스턴스이지만, 참조 형식의 배열은 배열 요소가 참조 형식이 할당된 메모리를 가리키는 주소의 값이 들어 있다. 값 형식의 배열이 참조 형식의 배열보다 메모리 할당 및 해제가 저렴하고, 값 형식의 배열이 참조 집약성이 대부분 더 좋다.
3) 값 형식은 참조 형식 또는 인터페이스로 캐스팅 될 때 방식이 발생하고, 다시 값으로 캐스팅 될 때 언방식이 발생한다.
4) 참조 형식은 참조를 복사하지만, 값 형식은 전체 값을 복사한다. 데이터가 큰 할당은 참조 형식이 값 형식 보다 성능이 좋다.
5) 참조 형식은 인스턴스를 변경하면 인스턴스를 가리키는 모든 참조에 영향을 주지만, 값 형식은 복사로 전달 되기 때문에 서로간의 영향을 주지 않는다.
아래의 특징이 없는 경우 구조체 대신 클래스를 사용해야 한다.
ㄱ) 박싱이 자주 발생하지 않는다.
ㄴ) 값이 변경되지 않는다.
ㄷ) 인스턴스의 크기가 16바이트 이하이다.
: 레지스터 하나로 처리할 수 있는 데이터의 크기가 16바이트이기 때문에 권장 크기를 16바이트로 하는 것
ㄹ) 단일 값으로 표현되는 데이터이다. ( int, double, float 등 )
* 출처 및 참고 블로그
https://ko.wikipedia.org/wiki/%EC%BA%A1%EC%8A%90%ED%99%94
https://docs.microsoft.com/ko-kr/dotnet/csharp/language-reference/builtin-types/struct
http://www.csharpstudy.com/Mistake/Article/10
https://www.sysnet.pe.kr/2/0/12620
https://www.sysnet.pe.kr/2/0/12624
https://docs.microsoft.com/ko-kr/dotnet/standard/design-guidelines/choosing-between-class-and-struct