구조체가 C #을 통해 CLR에서 인터페이스를 구현하는 것이 얼마나 나쁜지에 대해 읽은 것을 기억하는 것 같지만 그것에 대해 아무것도 찾을 수없는 것 같습니다. 나쁜가요? 그렇게하면 의도하지 않은 결과가 있습니까?
public interface Foo { Bar GetBar(); }
public struct Fubar : Foo { public Bar GetBar() { return new Bar(); } }
답변:
이 질문에는 몇 가지 일이 있습니다 ...
구조체가 인터페이스를 구현하는 것이 가능하지만 캐스팅, 변경 가능성 및 성능과 관련된 문제가 있습니다. 자세한 내용은이 게시물을 참조하십시오. https://docs.microsoft.com/en-us/archive/blogs/abhinaba/c-structs-and-interface
일반적으로 구조체는 값 유형 의미를 가진 객체에 사용되어야합니다. 구조체에 인터페이스를 구현하면 구조체가 구조체와 인터페이스 사이에서 앞뒤로 캐스트되므로 권투 문제가 발생할 수 있습니다. 박싱의 결과로 구조체의 내부 상태를 변경하는 작업이 제대로 작동하지 않을 수 있습니다.
IComparable<T>
및 IEquatable<T>
. 구조체 Foo
를 유형의 변수에 저장 IComparable<Foo>
하려면 boxing이 필요하지만 제네릭 형식 T
이 IComparable<T>
하나에 제한되어 있으면 하나를 boxing T
하지 않고 T
제약 조건을 구현하는 것 외에 다른 것을 알 필요없이 다른 형식 과 비교할 수 있습니다 . 이러한 유리한 동작은 인터페이스를 구현하는 구조체의 능력에 의해서만 가능합니다. 그 말은 ...
이 답변을 명시 적으로 제공 한 사람이 없기 때문에 다음을 추가합니다.
구현 구조체의 인터페이스 것은 어떠한 부정의 결과가 없습니다.
구조체를 보유하는 데 사용되는 인터페이스 유형의 모든 변수 는 해당 구조체의 박스형 값이 사용됩니다. 구조체가 변경 불가능한 경우 (좋은 점) 다음과 같은 경우가 아니면 최악의 성능 문제입니다.
둘 다 가능성이 낮고 대신 다음 중 하나를 수행 할 가능성이 있습니다.
인터페이스를 구현하는 구조체에 대한 많은 합리적인 이유는 제약 조건이 있는 일반 컨텍스트 내에서 사용할 수 있기 때문일 것 입니다. 이 방식으로 사용할 때 변수는 다음과 같습니다.
class Foo<T> : IEquatable<Foo<T>> where T : IEquatable<T>
{
private readonly T a;
public bool Equals(Foo<T> other)
{
return this.a.Equals(other.a);
}
}
new()
또는 같은 다른 제약 조건 class
이 사용 되지 않는 한 .그러면 this.a는 인터페이스 참조가 아니므로 그 안에 배치 된 상자를 생성하지 않습니다. 또한 C # 컴파일러가 제네릭 클래스를 컴파일하고 Type 매개 변수 T의 인스턴스에 정의 된 인스턴스 메서드의 호출을 삽입해야하는 경우 제한된 opcode를 사용할 수 있습니다 .
thisType이 값 유형이고 thisType이 메소드를 구현하는 경우 ptr은 thisType에 의한 메소드 구현을 위해 호출 메소드 명령에 대한 'this'포인터로 수정되지 않은 상태로 전달됩니다.
이것은 boxing을 피하고 값 유형이 구현하기 때문에 인터페이스는 메소드 를 구현 해야 하므로 boxing이 발생하지 않습니다. 위의 예에서 Equals()
호출은 this.a 1에 상자없이 수행됩니다 .
대부분의 구조체는 비트 단위로 동일한 값이 2 로 간주되는 원시적 의미 체계를 가져야 합니다. 런타임은 이러한 동작을 암시 적으로 제공 Equals()
하지만 속도가 느릴 수 있습니다. 또한 이러한 암시 적 동등성은 의 구현으로 노출 되지 않으므로IEquatable<T>
명시 적으로 구현하지 않는 한 사전의 키로 구조체가 쉽게 사용되는 것을 방지합니다. 따라서 많은 공용 구조체 형식 이 CLR BCL 내의 많은 기존 값 형식의 동작과 일치 할뿐만 아니라 더 쉽고 더 나은 성능을 제공하기 위해 구현 IEquatable<T>
( T
자체 위치 ) 을 선언하는 것이 일반적 입니다.
BCL의 모든 기본 요소는 최소한 다음을 구현합니다.
IComparable
IConvertible
IComparable<T>
IEquatable<T>
(따라서 IEquatable
)많은 사람들 IFormattable
이. 복소수 구조체 또는 일부 고정 너비 텍스트 값과 같이 유사하게 '넓게 유용한'유형을 구현하는 경우 이러한 공통 인터페이스를 많이 구현하면 (올바르게) 구조체가 더 유용하고 사용 가능해집니다.
분명히 인터페이스가 변경 가능성을 강하게 암시하는 경우 (예 :)ICollection
구현하는 것은 구조체를 변경 가능하게 만들 었는지 여부를 의미 하므로 나쁜 생각입니다 (원본이 아닌 박스형 값에서 수정이 발생하는 곳에서 이미 설명 된 종류의 오류로 이어집니다. ) 또는 Add()
예외 와 같은 메소드의 의미를 무시하여 사용자를 혼란스럽게합니다 .
많은 인터페이스는 변경 가능성 (예 :)을 의미하지 않으며 IFormattable
일관된 방식으로 특정 기능을 노출하는 관용적 방법으로 사용됩니다. 종종 구조체의 사용자는 이러한 동작에 대한 복싱 오버 헤드에 대해 신경 쓰지 않습니다.
불변 값 유형에 대해 현명하게 수행하면 유용한 인터페이스를 구현하는 것이 좋습니다.
1 : 컴파일러는 특정 구조체 유형 으로 알려져 있지만 가상 메서드를 호출해야하는 변수에 대해 가상 메서드를 호출 할 때 이것을 사용할 수 있습니다 . 예를 들면 :
List<int> l = new List<int>();
foreach(var x in l)
;//no-op
List에 의해 반환 된 열거자는 목록을 열거 할 때 할당을 피하기위한 최적화 인 구조체입니다 (일부 흥미로운 결과 포함 ). 열거 구현이 경우의 foreach의 의미는 그 지정 IDisposable
후 Dispose()
반복이 완료되면 호출됩니다. 분명히 이것이 boxed 호출을 통해 발생하면 열거자가 구조체 인 이점을 제거 할 수 있습니다 (실제로 더 나쁠 것입니다). 더 나쁜 것은 dispose 호출이 어떤 식 으로든 열거 자의 상태를 수정하는 경우 박스형 인스턴스에서 발생하고 복잡한 경우 많은 미묘한 버그가 발생할 수 있습니다. 따라서 이러한 상황에서 방출되는 IL은 다음과 같습니다.
IL_0001 : newobj System.Collections.Generic.List..ctor IL_0006 : stloc.0 IL_0007 : 아니요 IL_0008 : ldloc.0 IL_0009 : callvirt System.Collections.Generic.List.GetEnumerator IL_000E : stloc.2 IL_000F : br.s IL_0019 IL_0011 : ldloca.s 02 IL_0013 : System.Collections.Generic.List.get_Current 호출 IL_0018 : stloc.1 IL_0019 : ldloca.s 02 IL_001B : System.Collections.Generic.List.MoveNext 호출 IL_0020 : stloc.3 IL_0021 : ldloc.3 IL_0022 : brtrue.s IL_0011 IL_0024 : 떠나다 .s IL_0035 IL_0026 : ldloca.s 02 IL_0028 : 제한됨. System.Collections.Generic.List.Enumerator IL_002E : callvirt System.IDisposable.Dispose IL_0033 : 아니요 IL_0034 : 최종적으로
따라서 IDisposable의 구현은 성능 문제를 일으키지 않으며 Dispose 메서드가 실제로 어떤 작업을 수행하는 경우 열거 자의 (유감스러운) 변경 가능한 측면이 유지됩니다!
2 : double 및 float는 NaN 값이 같지 않은 것으로 간주되는이 규칙의 예외입니다.
struct
에 interface
.
어떤 경우에는 구조체가 인터페이스를 구현하는 것이 좋을 수 있습니다 (만약 유용하지 않았다면 .net 제작자가 제공했을 것임이 의심 스럽습니다). 구조체가와 같은 읽기 전용 인터페이스를 구현 IEquatable<T>
하는 경우 구조체를 유형의 저장 위치 (변수, 매개 변수, 배열 요소 등)에 저장 IEquatable<T>
하려면 박스형이어야합니다 (각 구조체 유형은 실제로 두 가지 종류를 정의합니다. 값 유형으로 작동하는 위치 유형 및 클래스 유형으로 작동하는 힙 오브젝트 유형. 첫 번째는 암시 적으로 두 번째 ( "boxing")로 변환 가능하며 두 번째는 명시 적 캐스트를 통해 첫 번째로 변환 될 수 있습니다. "unboxing"). 그러나 제약 된 제네릭이라고하는 것을 사용하여 boxing없이 인터페이스의 구조 구현을 이용할 수 있습니다.
예를 들어 메서드가있는 경우 CompareTwoThings<T>(T thing1, T thing2) where T:IComparable<T>
이러한 메서드는 thing1.Compare(thing2)
box thing1
또는 을 호출 할 필요없이 호출 할 수 thing2
있습니다. 경우 thing1
, 예를 될 일이, Int32
, 런타임은 대한 코드를 생성 할 때 알게 될 것이다 CompareTwoThings<Int32>(Int32 thing1, Int32 thing2)
. 메서드를 호스팅하는 항목과 매개 변수로 전달되는 항목의 정확한 유형을 모두 알 수 있으므로 둘 중 하나를 상자에 넣을 필요가 없습니다.
인터페이스를 구현하는 구조체의 가장 큰 문제는 인터페이스 유형 Object
, 또는 ValueType
(자체 유형의 위치와 반대) 의 위치에 저장되는 구조체가 클래스 객체로 동작한다는 것입니다. 읽기 전용 인터페이스의 경우 일반적으로 문제가되지 않지만 이와 같은 변형 인터페이스의 IEnumerator<T>
경우 이상한 의미를 생성 할 수 있습니다.
예를 들어 다음 코드를 고려하십시오.
List<String> myList = [list containing a bunch of strings]
var enumerator1 = myList.GetEnumerator(); // Struct of type List<String>.IEnumerator
enumerator1.MoveNext(); // 1
var enumerator2 = enumerator1;
enumerator2.MoveNext(); // 2
IEnumerator<string> enumerator3 = enumerator2;
enumerator3.MoveNext(); // 3
IEnumerator<string> enumerator4 = enumerator3;
enumerator4.MoveNext(); // 4
표시된 문 # 1은 enumerator1
첫 번째 요소를 읽을 수 있습니다. 해당 열거 자의 상태가에 복사됩니다 enumerator2
. 표시된 문 # 2는 두 번째 요소를 읽기 위해 해당 사본을 진행하지만에 영향을주지 않습니다 enumerator1
. 두 번째 열거 자의 상태가에 복사 enumerator3
되고 표시된 문 # 3에 의해 진행됩니다. 때문에 그 다음, enumerator3
그리고 enumerator4
두 참조 형식하는이다 참고 로 enumerator3
다음에 복사됩니다이 enumerator4
때문에 표시된 문 효과적으로 진출하게됩니다 모두 enumerator3
와 enumerator4
.
어떤 사람들은 값 유형과 참조 유형이 둘 다 유형 인 척하려고 Object
하지만 실제로는 그렇지 않습니다. 실제 값 유형은로 변환 할 수 Object
있지만 인스턴스가 아닙니다. List<String>.Enumerator
해당 유형의 위치에 저장된 인스턴스 는 값 유형이며 값 유형으로 작동합니다. 유형의 위치에 복사하면 IEnumerator<String>
참조 유형으로 변환되고 참조 유형으로 작동합니다 . 후자는 일종의 Object
이지만 전자는 그렇지 않습니다.
BTW, 몇 가지 추가 참고 사항 : (1) 일반적으로 변경 가능한 클래스 유형은 Equals
메서드가 참조 동등성을 테스트 해야 하지만 boxed struct가 그렇게 할 수있는 적절한 방법은 없습니다. (2) 이름에도 불구하고 ValueType
값 유형이 아닌 클래스 유형입니다. 에서 파생 된 System.Enum
모든 유형은를 ValueType
제외하고 파생 된 모든 유형과 마찬가지로 값 유형 System.Enum
이지만 ValueType
및 둘 다 System.Enum
클래스 유형입니다.
구조체는 값 유형으로 구현되고 클래스는 참조 유형입니다. Foo 유형의 변수가 있고 여기에 Fubar의 인스턴스를 저장하면 참조 유형으로 "Box it"되므로 처음에 구조체를 사용하는 이점이 없습니다.
클래스 대신 구조체를 사용하는 유일한 이유는 참조 형식이 아닌 값 형식이지만 구조체는 클래스에서 상속 할 수 없기 때문입니다. 구조체가 인터페이스를 상속하고 인터페이스를 전달하면 구조체의 해당 값 유형 특성을 잃게됩니다. 인터페이스가 필요한 경우 클래스로 만들 수도 있습니다.
(추가 할 전공은 없지만 아직 편집 능력이 없으므로 여기에 있습니다.)
완벽하게 안전합니다. 구조체에 인터페이스를 구현하는 데 불법은 없습니다. 그러나 왜 그렇게하고 싶은지 의문을 가져야합니다.
그러나 구조체에 대한 인터페이스 참조를 얻으면 BOX가 됩니다. 따라서 성능 저하 등이 있습니다.
내가 지금 생각할 수있는 유일한 유효한 시나리오는 여기 내 게시물에 설명되어 있습니다 . 컬렉션에 저장된 구조체의 상태를 수정하려면 구조체에 노출 된 추가 인터페이스를 통해 수정해야합니다.
Int32
제네릭 형식 받아들이는 방법 T:IComparable<Int32>
(중 제네릭 형식의 메서드의 매개 변수 또는 메소드의 클래스가 될 수 있음), 그 방법은 사용할 수 있습니다 Compare
에 방법을 전달 된 객체를 권투없이.
문제는 구조체가 값 유형이므로 약간의 성능 저하가 있기 때문에 권투가 발생한다는 것입니다.
이 링크는 다른 문제가있을 수 있음을 나타냅니다.
http://blogs.msdn.com/abhinaba/archive/2005/10/05/477238.aspx
값 유형이 인터페이스를 구현할 이유가 거의 없습니다. 값 유형을 하위 클래스로 분류 할 수 없기 때문에 항상이를 구체적인 유형으로 참조 할 수 있습니다.
물론 동일한 인터페이스를 모두 구현하는 여러 구조체가 있지 않는 한 약간 유용 할 수 있지만 그 시점에서 클래스를 사용하고 올바르게 수행하는 것이 좋습니다.
물론 인터페이스를 구현함으로써 구조체를 박싱하는 것이므로 이제는 힙에 있고 더 이상 값으로 전달할 수 없습니다 ... 이것은 클래스를 사용해야한다는 제 의견을 강화합니다. 이러한 상황에서.
IComparable
값을 상자에 넣을 필요가 없습니다 . IComparable
이를 구현하는 값 형식으로 예상하는 메서드를 호출하면 값 형식을 암시 적으로 상자에 넣을 수 있습니다.
IComparable<T>
형식의 구조에서 메서드를 호출 할 수 있습니다 T
.
구조체는 스택에있는 클래스와 같습니다. 나는 그들이 "안전하지 않은"이유를 알지 못한다.