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


95

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

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

답변:


45

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

구조체가 인터페이스를 구현하는 것이 가능하지만 캐스팅, 변경 가능성 및 성능과 관련된 문제가 있습니다. 자세한 내용은이 게시물을 참조하십시오. https://docs.microsoft.com/en-us/archive/blogs/abhinaba/c-structs-and-interface

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


3
"박싱의 결과로 구조체의 내부 상태를 변경하는 작업이 제대로 작동하지 않을 수 있습니다." 예를 들어 답을 얻으십시오.

2
@Will : 댓글에서 무엇을 언급하고 있는지 확실하지 않습니다. 내가 참조한 블로그 게시물에는 구조체에서 인터페이스 메서드를 호출해도 실제로 내부 값이 변경되지 않는 예가 있습니다.
Scott Dorman

12
@ScottDorman : 어떤 경우에는 구조가 인터페이스를 구현 하도록하면 권투 를 피하는 데 도움이 될 수 있습니다 . 주요 예는 IComparable<T>IEquatable<T>. 구조체 Foo를 유형의 변수에 저장 IComparable<Foo>하려면 boxing이 필요하지만 제네릭 형식 TIComparable<T>하나에 제한되어 있으면 하나를 boxing T하지 않고 T제약 조건을 구현하는 것 외에 다른 것을 알 필요없이 다른 형식 과 비교할 수 있습니다 . 이러한 유리한 동작은 인터페이스를 구현하는 구조체의 능력에 의해서만 가능합니다. 그 말은 ...
supercat

3
... 특정 인터페이스가 unboxed 구조에만 적용 가능한 것으로 간주되어야한다고 선언하는 수단이 있다면 좋았을 것입니다. 클래스 객체 나 boxed 구조가 원하는 것을 가질 수없는 일부 컨텍스트가 있기 때문입니다. 행동.
supercat

2
"구조체는 값 유형의 의미를 가진 객체에 사용되어야합니다. ... 구조체의 내부 상태를 변경하는 작업이 제대로 작동하지 않을 수 있습니다." 가치 형 의미론과 가변성이 잘 섞이지 않는다는 사실이 진짜 문제 아닙니까?
jpmc26

186

이 답변을 명시 적으로 제공 한 사람이 없기 때문에 다음을 추가합니다.

구현 구조체의 인터페이스 것은 어떠한 부정의 결과가 없습니다.

구조체를 보유하는 데 사용되는 인터페이스 유형의 모든 변수 는 해당 구조체의 박스형 값이 사용됩니다. 구조체가 변경 불가능한 경우 (좋은 점) 다음과 같은 경우가 아니면 최악의 성능 문제입니다.

  • 잠금 목적으로 결과 객체 사용 (어쨌든 매우 나쁜 생각)
  • 참조 같음 의미 체계를 사용하고 동일한 구조체의 두 개의 박스형 값에 대해 작동 할 것으로 예상합니다.

둘 다 가능성이 낮고 대신 다음 중 하나를 수행 할 가능성이 있습니다.

제네릭

인터페이스를 구현하는 구조체에 대한 많은 합리적인 이유는 제약 조건이 있는 일반 컨텍스트 내에서 사용할 수 있기 때문일 것 입니다. 이 방식으로 사용할 때 변수는 다음과 같습니다.

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의 인스턴스에 정의 된 인스턴스 메서드의 호출을 삽입해야하는 경우 제한된 opcode를 사용할 수 있습니다 .

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

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

저 마찰 API

대부분의 구조체는 비트 단위로 동일한 값이 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의 의미는 그 지정 IDisposableDispose()반복이 완료되면 호출됩니다. 분명히 이것이 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 값이 같지 않은 것으로 간주되는이 규칙의 예외입니다.


1
egheadcafe.com 사이트가 이전되었지만 그 내용을 잘 유지하지 못했습니다. 시도했지만 eggheadcafe.com/software/aspnet/31702392/… 의 원본 문서를 찾을 수 없습니다 . OP에 대한 지식이 부족합니다. (훌륭한 요약을위한 PS +1).
Abel

2
이것은 좋은 대답이지만 "요약"을 "TL; DR"로 맨 위로 이동하여 개선 할 수 있다고 생각합니다. 결론을 먼저 제공하면 독자가 당신이 사물을 어디로 가고 있는지 알 수 있습니다.
Hans

를 캐스팅 할 때 컴파일러 경고가 있어야한다 structinterface.
Jalal

8

어떤 경우에는 구조체가 인터페이스를 구현하는 것이 좋을 수 있습니다 (만약 유용하지 않았다면 .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때문에 표시된 문 효과적으로 진출하게됩니다 모두 enumerator3enumerator4.

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

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


3

구조체는 값 유형으로 구현되고 클래스는 참조 유형입니다. Foo 유형의 변수가 있고 여기에 Fubar의 인스턴스를 저장하면 참조 유형으로 "Box it"되므로 처음에 구조체를 사용하는 이점이 없습니다.

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


인터페이스를 구현하는 기본 요소에도 이와 같이 작동합니까?
aoetalks

3

(추가 할 전공은 없지만 아직 편집 능력이 없으므로 여기에 있습니다.)
완벽하게 안전합니다. 구조체에 인터페이스를 구현하는 데 불법은 없습니다. 그러나 왜 그렇게하고 싶은지 의문을 가져야합니다.

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

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


하나가를 통과하면 Int32제네릭 형식 받아들이는 방법 T:IComparable<Int32>(중 제네릭 형식의 메서드의 매개 변수 또는 메소드의 클래스가 될 수 있음), 그 방법은 사용할 수 있습니다 Compare에 방법을 전달 된 객체를 권투없이.
supercat


0

인터페이스를 구현하는 구조체에는 아무런 결과가 없습니다. 예를 들어 내장 된 시스템 구조체 구현 인터페이스를 좋아 IComparable하고 IFormattable.


0

값 유형이 인터페이스를 구현할 이유가 거의 없습니다. 값 유형을 하위 클래스로 분류 할 수 없기 때문에 항상이를 구체적인 유형으로 참조 할 수 있습니다.

물론 동일한 인터페이스를 모두 구현하는 여러 구조체가 있지 않는 한 약간 유용 할 수 있지만 그 시점에서 클래스를 사용하고 올바르게 수행하는 것이 좋습니다.

물론 인터페이스를 구현함으로써 구조체를 박싱하는 것이므로 이제는 힙에 있고 더 이상 값으로 전달할 수 없습니다 ... 이것은 클래스를 사용해야한다는 제 의견을 강화합니다. 이러한 상황에서.


구체적인 구현 대신 IComparable을 얼마나 자주 전달합니까?
FlySwat

IComparable값을 상자에 넣을 필요가 없습니다 . IComparable이를 구현하는 값 형식으로 예상하는 메서드를 호출하면 값 형식을 암시 적으로 상자에 넣을 수 있습니다.
Andrew Hare

1
@AndrewHare : 제한된 제네릭을 사용하면 boxing없이 IComparable<T>형식의 구조에서 메서드를 호출 할 수 있습니다 T.
supercat

-10

구조체는 스택에있는 클래스와 같습니다. 나는 그들이 "안전하지 않은"이유를 알지 못한다.


상속이 부족하다는 점을 제외하면.
FlySwat

7
이 답변의 모든 부분에 동의하지 않습니다. 그것들 반드시 스택에 있을 필요 는 없으며 카피 시맨틱은 클래스와 매우 다릅니다 .
Marc Gravell

1
그들은, 구조체의 과도한 사용이 :( 당신의 기억은 슬픈 불변 할 것입니다
테오 만 shipahi

1
@Teomanshipahi 클래스 인스턴스를 과도하게 사용하면 가비지 수집기가 화가 나게됩니다.
IllidanS4 모니카 지원

4
2 만 회 이상인 사람에게이 대답은 받아 들일 수 없습니다.
Krythic
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.