it-gundan.com

구조체가 인터페이스를 구현하는 것이 안전합니까?

구조체가 C #을 통해 CLR에서 인터페이스를 구현하는 것이 좋지 않은 방법에 대해 읽은 것을 기억하는 것 같지만 아무것도 찾을 수없는 것 같습니다. 나빠요? 의도하지 않은 결과가 있습니까?

public interface Foo { Bar GetBar(); }
public struct Fubar : Foo { public Bar GetBar() { return new Bar(); } }
83
Will

이 질문에는 몇 가지 일이 있습니다 ...

구조체가 인터페이스를 구현하는 것이 가능하지만 캐스팅, 변경 가능성 및 성능과 관련된 문제가 있습니다. 자세한 내용은이 게시물을 참조하십시오. http://blogs.msdn.com/abhinaba/archive/2005/10/05/477238.aspx

일반적으로 구조체는 값 형식 의미가있는 개체에 사용해야합니다. 구조체에서 인터페이스를 구현하면 구조체와 인터페이스 사이에서 구조체가 앞뒤로 캐스팅되므로 권투 문제가 발생할 수 있습니다. 복싱으로 인해 구조체의 내부 상태를 변경하는 작업이 제대로 작동하지 않을 수 있습니다.

45
Scott Dorman

아무도이 답변을 명시 적으로 제공하지 않았으므로 다음을 추가합니다.

구조의 인터페이스를 구현하는 부정적인 결과는 없습니다.

구조체를 보유하는 데 사용되는 인터페이스 유형의 variable을 사용하면 해당 구조체의 상자 값이 사용됩니다. 구조체가 불변 (좋은 것)이라면 다음과 같은 경우가 아니면 최악의 성능 문제입니다.

  • 잠금 목적으로 결과 객체 사용 (어쨌든 나쁜 생각)
  • 참조 평등 의미론을 사용하고 동일한 구조체의 두 상자 값에 대해 작동 할 것으로 기대합니다.

이 두 가지가 모두 가능하지는 않지만 대신 다음 중 하나를 수행 할 가능성이 있습니다.

제네릭

아마도 인터페이스를 구현하는 구조체에 대한 많은 합리적인 이유는 constraints 와 함께 generic 컨텍스트 내에서 사용될 수 있기 때문입니다. . 이 방식으로 변수를 사용하면 다음과 같이 변수가됩니다.

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);
    }
}
  1. 구조체를 형식 매개 변수로 사용하도록 설정
    • new() 또는 class와 같은 다른 제한 조건이 사용되지 않는 한.
  2. 이런 식으로 사용되는 구조체에 권투를 피하십시오.

그러면 this.a는 인터페이스 참조가 아니므로 어떤 것에도 상자가 생기지 않습니다. 또한 c # 컴파일러가 일반 클래스를 컴파일하고 Type 매개 변수 T의 인스턴스에 정의 된 인스턴스 메소드의 호출을 삽입해야하는 경우 constrained opcode를 사용할 수 있습니다.

ThisType이 값 유형이고 thisType이 메소드를 구현하는 경우 ptr은 thisType에 의한 메소드 구현을 위해 호출 메소드 명령에 대한 'this'포인터로 수정되지 않은 채 전달됩니다.

이것은 권투를 피하고 값 유형이 인터페이스를 구현하기 때문에 must 메소드를 구현하므로 권투가 발생하지 않습니다. 위의 예에서 Equals() 호출은 this에 상자없이 수행됩니다.1.

저 마찰 API

대부분의 구조체에는 비트 단위의 동일한 값이 동일한 것으로 간주되는 프리미티브와 같은 의미론이 있어야합니다.2. 런타임은 암시 적 Equals()에서 이러한 동작을 제공하지만 느려질 수 있습니다. 또한이 암시 적 평등은 notIEquatable<T>의 구현으로 노출되어 있으므로 명시 적으로 구현하지 않는 한 구조체를 사전의 키로 쉽게 사용할 수 없습니다. 따라서 많은 공공 구조체 유형이 IEquatable<T>를 구현한다고 선언하는 것이 일반적입니다 (여기서 T는 자체입니다). CLR BCL 내의 유형.

BCL의 모든 기본 요소는 최소한 다음을 구현합니다.

  • IComparable
  • IConvertible
  • IComparable<T>
  • IEquatable<T> (따라서 IEquatable)

또한 많은 사람들이 IFormattable를 구현하고 있으며 DateTime, TimeSpan 및 Guid와 같은 많은 시스템 정의 값 유형은 이들 중 다수 또는 전부를 구현합니다. 복잡한 숫자 구조체 또는 고정 너비 텍스트 값과 같이 유사하게 광범위하게 유용한 유형을 구현하는 경우 이러한 공통 인터페이스를 올바르게 구현하면 구조체가 더 유용하고 사용 가능해집니다.

제외

인터페이스가 mutability (예 : ICollection)를 강력하게 암시하는 경우, 구조체를 변경 가능하게 만들었 음을 의미하는 나쁜 아이디어입니다. 원래 값이 아닌 상자 값에서 수정이 발생하는 위치에 이미 설명 된 오류) 또는 Add()과 같은 메서드의 의미를 무시하거나 예외를 throw하여 사용자를 혼동합니다.

많은 인터페이스는 변경 가능성 (예 : IFormattable)을 의미하지 않으며 특정 기능을 일관된 방식으로 노출하는 관용적 방법으로 사용됩니다. 종종 구조체의 사용자는 그러한 행동에 대한 권투 오버 헤드에 신경 쓰지 않을 것입니다.

요약

불변의 값 유형에서 현명하게 수행되면 유용한 인터페이스를 구현하는 것이 좋습니다


노트:

1 : 컴파일러가 known 변수에 대해 가상 메소드를 호출 할 때 특정 구조체 유형이지만 가상 메소드를 호출해야하는 경우이를 사용할 수 있습니다. 예를 들면 다음과 같습니다.

List<int> l = new List<int>();
foreach(var x in l)
    ;//no-op

List에 의해 리턴 된 열거자는리스트를 열거 할 때 할당을 피하기위한 최적화 된 구조체입니다 (몇 가지 흥미로운 consequences ). 그러나 foreach의 의미는 열거자가 IDisposable를 구현하면 반복이 완료되면 Dispose()이 호출되도록 지정합니다. 분명히 박스형 호출을 통해 이러한 일이 발생하면 열거자가 구조체라는 이점이 사라집니다 (사실 더 나쁩니다). 더 나쁜 것은, dispose 콜이 어떤 식 으로든 열거 자의 상태를 수정하면 박스형 인스턴스에서 발생하고 복잡한 경우에는 많은 미묘한 버그가 발생할 수 있습니다. 따라서 이런 상황에서 방출되는 IL은 다음과 같습니다.

 IL_0001 : newobj System.Collections.Generic.List..ctor 
 IL_0006 : stloc.0 
 IL_0007 : nop 
 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 : leave.s IL_0035 
 IL_0026 : ldloca .s 02 
 IL_0028 : 제한됨. System.Collections.Generic.List.Enumerator 
 IL_002E : callvirt System.IDisposable.Dispose 
 IL_0033 : nop 
 IL_0034 : 최종적으로 

따라서 IDisposable을 구현해도 성능 문제가 발생하지 않으며 Dispose 메서드가 실제로 수행하는 경우 열거 자의 (유감스러운) 변경 가능한 측면이 보존됩니다!

2 : NaN 값이 같은 것으로 간주되지 않는 double 및 float은이 규칙에서 예외입니다.

161
ShuggyCoUk

어떤 경우에는 struct가 인터페이스를 구현하는 것이 좋을 수도 있습니다 (유용하지 않은 경우 .net의 작성자가 제공했을 가능성이 의심됩니다). 구조체가 IEquatable<T>와 같은 읽기 전용 인터페이스를 구현하는 경우, 구조체를 IEquatable<T> 유형의 저장 위치 (변수, 매개 변수, 배열 요소 등)에 저장하려면 상자를 채워야합니다 ( 각 구조체 유형은 실제로 값 유형으로 작동하는 저장 위치 유형과 클래스 유형으로 작동하는 힙 객체 유형의 두 가지 종류를 정의합니다. 두 번째는 명시 적 캐스트 ( "unboxing")를 통해 첫 번째로 변환 될 수 있습니다. 그러나 제약이없는 제네릭이라고하는 것을 사용하여 복싱없이 인터페이스의 구조 구현을 활용할 수 있습니다.

예를 들어, CompareTwoThings<T>(T thing1, T thing2) where T:IComparable<T> 메소드가있는 경우 이러한 메소드는 thing1 또는 thing2 상자없이 thing1.Compare(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으로 진행됩니다. 그런 다음 enumerator3enumerator4는 모두 참조 유형이므로 REFERENCE ~ enumerator3enumerator4에 복사되므로 표시된 명령문은 효과적으로 bothenumerator3enumerator4로 진행됩니다.

어떤 사람들은 값 유형과 참조 유형이 모두 Object의 종류 인 것처럼 가장하려고하지만 실제로는 그렇지 않습니다. 실제 값 유형은 Object로 변환 할 수 있지만 해당 인스턴스는 아닙니다. 해당 유형의 위치에 저장된 List<String>.Enumerator 인스턴스는 값 유형이며 값 유형으로 작동합니다. IEnumerator<String> 유형의 위치에 복사하면 참조 유형으로 변환되고 참조 유형으로 동작. 후자는 일종의 Object이지만 전자는 그렇지 않습니다.

BTW, 몇 가지 참고 사항 : (1) 일반적으로 변경 가능한 클래스 유형은 Equals 메소드의 참조 동등성을 테스트해야하지만, 박스 구조에서 그렇게하는 적절한 방법은 없습니다. (2) 이름에도 불구하고 ValueType는 값 유형이 아닌 클래스 유형입니다. System.Enum에서 파생 된 모든 유형은 System.Enum를 제외하고 ValueType에서 파생 된 모든 유형과 마찬가지로 ValueTypeSystem.Enum는 수업 유형입니다.

8
supercat

(글쎄 추가 할 중요한 것은 없지만 아직 편집 능력은 없습니다.)
완전히 안전합니다. 구조체에 인터페이스를 구현하는 것은 불법이 아닙니다. 그러나 왜 그렇게하고 싶은지 질문해야합니다.

그러나 구조체에 대한 인터페이스 참조를 얻으면 BOX 됩니다. 성능 저하 등.

내가 지금 생각할 수있는 유일한 유효한 시나리오는 내 게시물에 설명되어 있음 입니다. 컬렉션에 저장된 구조체의 상태를 수정하려면 구조체에 노출 된 추가 인터페이스를 통해 수정해야합니다.

3
Gishu

구조체는 값 형식으로 구현되며 클래스는 참조 형식입니다. Foo 유형의 변수가 있고 해당 인스턴스에 Fubar 인스턴스를 저장하면 참조 유형으로 "Boxed"되어 구조를 처음 사용할 때의 이점이 사라집니다.

클래스 대신 구조체를 사용하는 유일한 이유는 그것이 값 형식이고 참조 형식이 아니기 때문에 구조체가 클래스에서 상속 할 수 없기 때문입니다. 구조체가 인터페이스를 상속하고 인터페이스를 전달하면 구조체의 해당 값 유형 특성이 손실됩니다. 인터페이스가 필요한 경우 클래스로 만들 수도 있습니다.

3
dotnetengineer

문제는 구조체가 값 유형이므로 약간의 성능 저하가 있기 때문에 권투가 발생한다는 것입니다.

이 링크는 다른 문제가있을 수 있음을 제안합니다 ...

http://blogs.msdn.com/abhinaba/archive/2005/10/05/477238.aspx

1
Simon Keep

인터페이스를 구현하는 구조체에는 아무런 영향이 없습니다. 예를 들어 내장 시스템 구조체는 IComparableIFormattable와 같은 인터페이스를 구현합니다.

0
Joseph Daigle

값 유형이 인터페이스를 구현할 이유가 거의 없습니다. 값 유형을 서브 클래스화할 수 없으므로 항상 구체적인 유형으로 참조 할 수 있습니다.

물론 동일한 인터페이스를 구현하는 여러 구조체가 없다면 그다지 유용하지는 않지만 클래스를 사용하고 올바르게 수행하는 것이 좋습니다.

물론 인터페이스를 구현하면 구조체를 복싱하므로 이제는 힙에 앉아 더 이상 값으로 전달할 수 없습니다. 이러한 상황에서.

0
FlySwat