.NET의 구조체는 상속을 지원하지 않지만 이러한 방식으로 제한되는 이유 는 명확하지 않습니다 .
구조체가 다른 구조체에서 상속되는 것을 막는 기술적 이유는 무엇입니까?
.NET의 구조체는 상속을 지원하지 않지만 이러한 방식으로 제한되는 이유 는 명확하지 않습니다 .
구조체가 다른 구조체에서 상속되는 것을 막는 기술적 이유는 무엇입니까?
답변:
값 유형이 상속을 지원할 수없는 이유는 배열 때문입니다.
문제는 성능 및 GC 이유로 인해 값 유형의 배열이 "인라인"으로 저장된다는 것입니다. 예를 들어, 소정의 new FooType[10] {...}
경우, FooType
참조 형식 11 개체는 관리 힙 (각 유형의 인스턴스의 배열 한, 10)에 생성한다. 경우 FooType
값 대신 타입 인스턴스 만이 관리 힙에 생성 될 것이다 - 배열 자체 (각 배열의 값으로서 배열 "인라인"를 저장한다).
이제 값 유형이있는 상속이 있다고 가정합니다. 위의 배열의 "인라인 스토리지"동작과 결합하면 C ++에서 볼 수 있듯이 나쁜 일이 발생 합니다.
이 의사 C # 코드를 고려하십시오.
struct Base
{
public int A;
}
struct Derived : Base
{
public int B;
}
void Square(Base[] values)
{
for (int i = 0; i < values.Length; ++i)
values [i].A *= 2;
}
Derived[] v = new Derived[2];
Square (v);
정상적인 변환 규칙에 따라 a Derived[]
는 Base[]
(더 좋든 나쁘 든) a 로 변환 할 수 있으므로 위의 예에서 s / struct / class / g하면 문제없이 예상대로 컴파일되고 실행됩니다. 그러나 경우 Base
와 Derived
값 종류가 있으며, 배열은 우리가 문제가, 인라인 값을 저장합니다.
에 Square()
대해 아무것도 모르기 때문에 문제 Derived
가 있습니다. 배열의 각 요소에 액세스하기 위해 포인터 산술 만 사용하여 일정한 양 ( sizeof(A)
) 만큼 증가합니다 . 어셈블리는 모호하게 다음과 같습니다.
for (int i = 0; i < values.Length; ++i)
{
A* value = (A*) (((char*) values) + i * sizeof(A));
value->A *= 2;
}
(예, 그것은 끔찍한 어셈블리이지만, 요점은 파생 된 형식이 사용되고 있다는 사실을 알지 못해도 알려진 컴파일 타임 상수에서 배열을 통해 증가한다는 것입니다.)
따라서 이것이 실제로 발생하면 메모리 손상 문제가 발생합니다. 특히, 내 Square()
, values[1].A*=2
것입니다 실제로 수정 될 values[0].B
!
디버깅하려고 THAT !
구조체가 상속을 지원한다고 상상해보십시오. 다음 선언 :
BaseStruct a;
InheritedStruct b; //inherits from BaseStruct, added fields, etc.
a = b; //?? expand size during assignment?
구조체 변수의 크기가 고정되어 있지 않다는 것을 의미하므로 참조 유형이 있습니다.
더 좋은 점은 다음과 같습니다.
BaseStruct[] baseArray = new BaseStruct[1000];
baseArray[500] = new InheritedStruct(); //?? morph/resize the array?
Foo
상속 Bar
을 사용하면 a Foo
가에 할당되는 것을 허용해서는 안되지만 Bar
이러한 방식으로 구조체를 선언하면 몇 가지 유용한 효과를 얻을 수 있습니다. (1) Bar
에서 첫 번째 항목으로 특수 이름의 멤버를 Foo
만들고 Foo
포함 멤버 이름에 그 회원들에게 별명 것을 Bar
사용했다 코드를 허용 Bar
적응 할 수는를 사용하는 Foo
모든 참조를 교체하지 않고, 대신 thing.BarMember
에 thing.theBar.BarMember
, 읽고 모두 쓰는 기능 유지 Bar
그룹으로 필드의 '; ...
구조체는 참조를 사용하지 않습니다 (박스형이 아니면 피해야합니다). 따라서 참조 포인터를 통한 간접 지정이 없기 때문에 다형성은 의미가 없습니다. 객체는 일반적으로 힙에 있으며 참조 포인터를 통해 참조되지만 구조체는 스택에 할당되거나 (박스형이 아닌 경우) 힙의 참조 유형이 차지하는 메모리 "내부"에 할당됩니다.
Foo
의 필드 를 가진 형식 (구조체 또는 클래스) 을 갖는 수단이 의 멤버를 자신의 Bar
것으로 간주 할 수 Bar
있다는 것입니다. Point3d
클래스 수 예를 들어, 캡슐화 Point2d xy
하지만이 참조 X
중 하나로 해당 필드의 xy.X
나 X
.
구조체는 값 의미가있는 작은 데이터 구조에 특히 유용합니다. 복소수, 좌표계의 점 또는 사전의 키-값 쌍은 모두 구조체의 좋은 예입니다. 이러한 데이터 구조의 핵심은 데이터 멤버가 적고 상속 또는 참조 ID를 사용할 필요가 없으며 할당이 참조 대신 값을 복사하는 값 의미 체계를 사용하여 편리하게 구현할 수 있다는 것입니다.
기본적으로 간단한 데이터를 보유해야하므로 상속과 같은 "추가 기능"이 없습니다. 기술적으로는 제한된 종류의 상속 (다형성이 아니라 스택에 있기 때문에)을 지원하는 것이 기술적으로 가능할 수 있지만 상속을 지원하지 않는 것이 설계 선택이라고 생각합니다 (.NET의 다른 많은 것들처럼) 언어가 있습니다.)
다른 한편으로 나는 상속의 이점에 동의하고 우리 모두가 struct
다른 사람으로부터 물려 받기를 원하는 지점에 도달했고 그것이 불가능하다는 것을 깨달았다 고 생각합니다. 그러나 그 시점에서 데이터 구조는 아마도 너무 발전하여 어쨌든 클래스가되어야합니다.
Point3D
A로부터를 Point2D
, 당신은 할 수 없을 것입니다 사용하는 Point3D
대신의를 Point2D
, 그러나 당신은 다시 구현하지 않았을 Point3D
...의 내가 어쨌든 그것을 해석하는 방법입니다) 완전히 처음부터.
class
을 통해 struct
적절한 경우.
구조체가 스택에 직접 배치되기 때문에 상속과 같은 클래스는 불가능합니다. 상속하는 구조체는 부모보다 더 클 것이지만 JIT는이를 모르고 너무 적은 공간에 너무 많은 것을 넣으려고합니다. 약간 불분명하게 들리면 예제를 작성해 보겠습니다.
struct A {
int property;
} // sizeof A == sizeof int
struct B : A {
int childproperty;
} // sizeof B == sizeof int * 2
이것이 가능하면 다음 스 니펫에서 충돌이 발생합니다.
void DoSomething(A arg){};
...
B b;
DoSomething(b);
공간은 B의 크기가 아니라 A의 크기에 할당됩니다.
수정하고 싶은 점이 있습니다. 구조체가 상속 될 수없는 이유는 스택에있는 구조가 올바른 것이기 때문이지만, 동일한 설명은 절반이 정확합니다. 다른 값 유형과 마찬가지로 구조체 는 스택에 있을 수 있습니다 . 변수가 선언 된 위치에 따라 다르기 때문에 스택 또는 힙에 위치 합니다. 이것은 그들이 각각 지역 변수 또는 인스턴스 필드 일 때입니다.
그렇게 말하면서 Cecil은 이름이 있습니다.
저는 이것을 강조하고 싶습니다. 값 유형 은 스택에 존재할 수 있습니다 . 그렇다고 항상 그렇게한다는 의미는 아닙니다. 메소드 매개 변수를 포함한 지역 변수가 사용됩니다. 다른 모든 것은 그렇지 않습니다. 그럼에도 불구하고 상속 될 수없는 이유는 여전히 남아 있습니다. :-)
구조체는 스택에 할당됩니다. 이것은 값 의미 체계가 거의 무료이며 구조체 멤버에 액세스하는 것이 매우 저렴하다는 것을 의미합니다. 이것은 다형성을 방지하지 않습니다.
가상 함수 테이블에 대한 포인터로 각 구조체를 시작할 수 있습니다. 이것은 성능 문제가 될 수 있지만 (모든 구조체는 적어도 포인터의 크기가 됨) 가능합니다. 이것은 가상 기능을 허용합니다.
필드 추가는 어떻습니까?
글쎄, 스택에 구조체를 할당 할 때 특정 공간을 할당합니다. 필요한 공간은 컴파일 시간에 결정됩니다 (사전 또는 JIT 팅시). 필드를 추가 한 다음 기본 유형에 할당하는 경우 :
struct A
{
public int Integer1;
}
struct B : A
{
public int Integer2;
}
A a = new B();
스택의 알려지지 않은 부분을 덮어 씁니다.
대안은 런타임이 A 변수에 sizeof (A) 바이트 만 기록하여이를 방지하는 것입니다.
B가 A의 메서드를 재정의하고 Integer2 필드를 참조하면 어떻게됩니까? 런타임에서 MemberAccessException이 발생하거나 메서드가 대신 스택의 일부 임의 데이터에 액세스합니다. 둘 다 허용되지 않습니다.
구조체를 다형성으로 사용하지 않거나 상속 할 때 필드를 추가하지 않는 한 구조체 상속을 사용하는 것이 완벽하게 안전합니다. 그러나 이것들은별로 유용하지 않습니다.
이것은 매우 빈번한 질문처럼 보입니다. 값 유형이 변수를 선언하는 "제자리에"저장된다는 점을 추가하고 싶습니다. 구현 세부 사항과는 별개로, 이것은 객체에 대해 어떤 것을 말하는 객체 헤더 가 없다는 것을 의미 하며 변수 만이 거기에 어떤 종류의 데이터가 있는지 알고 있습니다.
IL은 스택 기반 언어이므로 인수를 사용하여 메서드를 호출하면 다음과 같이됩니다.
메서드가 실행되면 스택에서 일부 바이트를 꺼내 인수를 얻습니다. 정확히 안다인수가 참조 유형 포인터 (32 비트에서는 항상 4 바이트)이거나 크기가 항상 정확하게 알려진 값 유형이기 때문에 얼마나 많은 바이트를 팝 오프할지 있습니다.
참조 유형 포인터 인 경우 메소드는 힙에서 객체를 찾고 정확한 유형에 대한 특정 메소드를 처리하는 메소드 테이블을 가리키는 유형 핸들을 가져옵니다. 값 유형 인 경우 값 유형이 상속을 지원하지 않기 때문에 메소드 테이블에 대한 조회가 필요하지 않으므로 가능한 메소드 / 유형 조합이 하나뿐입니다.
값 유형이 상속을 지원하는 경우 특정 유형의 구조체가 해당 값뿐만 아니라 스택에 배치되어야한다는 추가 오버 헤드가 발생합니다. 이는 유형의 특정 구체적인 인스턴스에 대한 일종의 메서드 테이블 조회를 의미합니다. 이것은 값 유형의 속도 및 효율성 이점을 제거합니다.