IT이야기

BCL 컬렉션이 클래스가 아닌 구조체 열거자를 사용하는 이유

cyworld 2021. 10. 1. 21:25
반응형

BCL 컬렉션이 클래스가 아닌 구조체 열거자를 사용하는 이유는 무엇입니까?


우리 모두는 가변 구조체가 일반적으로 악하다 는 것을 알고 있습니다. 나는 또한 IEnumerable<T>.GetEnumerator()type 을 반환 하기 때문에 IEnumerator<T>구조체가 참조 유형으로 즉시 boxing되어 시작하기 위해 단순히 참조 유형인 경우보다 더 많은 비용이 든다는 것을 확신합니다 .

그렇다면 BCL 제네릭 컬렉션에서 모든 열거자가 변경 가능한 구조체인 이유는 무엇입니까? 분명 그럴만한 이유가 있었을 것이다. 나에게 발생하는 유일한 것은 구조체를 쉽게 복사할 수 있으므로 임의의 지점에서 열거자 상태를 보존할 수 있다는 것입니다. 그러나 인터페이스에 Copy()메소드를 추가하는 IEnumerator것이 덜 번거로웠을 것입니다. 그래서 저는 이것이 그 자체로 논리적인 정당화라고 보지 않습니다.

비록 내가 디자인 결정에 동의하지 않는다 하더라도 그 뒤에 있는 이유를 이해할 수 있기를 바랍니다.


실제로 성능상의 이유입니다. BCL 팀은 의심스럽고 위험한 관행이라고 하는 것을 결정하기 전에 이 점에 대해 많은 조사를 했습니다. 즉, 변경 가능한 값 유형을 사용하는 것입니다.

이것이 권투를 일으키지 않는 이유를 묻습니다. C# 컴파일러는 foreach 루프에서 IEnumerable 또는 IEnumerator를 방지할 수 있는 경우 상자를 만드는 코드를 생성하지 않기 때문입니다!

우리가 볼 때

foreach(X x in c)

가장 먼저 할 일은 c에 GetEnumerator라는 메서드가 있는지 확인하는 것입니다. 그렇다면 반환하는 형식에 MoveNext 메서드와 current 속성이 있는지 확인합니다. 그렇다면 해당 메서드 및 속성에 대한 직접 호출을 사용하여 foreach 루프가 완전히 생성됩니다. "패턴"이 일치할 수 없는 경우에만 인터페이스를 찾는 것으로 대체합니다.

이것은 두 가지 바람직한 효과를 갖는다.

첫째, 컬렉션이 int 컬렉션이지만 제네릭 유형이 발명되기 전에 작성된 경우 Current 값을 object로 boxing한 다음 int로 unboxing하는 boxing 패널티를 받지 않습니다. Current가 int를 반환하는 속성이면 그냥 사용합니다.

둘째, 열거자가 값 형식이면 열거자를 IEnumerator로 boxing하지 않습니다.

내가 말했듯이 BCL 팀은 이것에 대해 많은 연구를 했고 대부분의 경우 열거자를 할당 및 할당 해제 하는 페널티가 충분히 커서 값 유형으로 만들 가치가 있음을 발견했습니다. 몇 가지 미친 버그를 유발합니다.

예를 들어 다음을 고려하십시오.

struct MyHandle : IDisposable { ... }
...
using (MyHandle h = whatever)
{
    h = somethingElse;
}

h를 변경하려는 시도가 실패할 것으로 예상하는 것은 당연하며 실제로 그렇습니다. 컴파일러는 보류 중인 삭제가 있는 항목의 값을 변경하려고 시도하고 있으며 그렇게 하면 삭제해야 하는 개체가 실제로 삭제되지 않을 수 있음을 감지합니다.

이제 다음이 있다고 가정합니다.

struct MyHandle : IDisposable { ... }
...
using (MyHandle h = whatever)
{
    h.Mutate();
}

여기서 무슨 일이? h가 읽기 전용 필드인 경우 컴파일러가 수행할 작업을 합리적으로 예상할 수 있습니다. 즉, 복사본을 만들고 메서드가 값에서 삭제해야 하는 항목을 버리지 않도록 복사본변경합니다 .

그러나 이것은 여기서 일어날 일에 대한 우리의 직관과 충돌합니다.

using (Enumerator enumtor = whatever)
{
    ...
    enumtor.MoveNext();
    ...
}

우리는 사용 블록 내부에 MoveNext는 일을하는 것으로 예상 됩니다 상관없이 구조체 또는 심판 유형인지의 다음 하나에 열거 이동합니다.

불행히도 오늘날 C# 컴파일러에는 버그가 있습니다. 이러한 상황에 처한 경우 일관성 없이 따라야 할 전략을 선택합니다. 오늘의 행동은 다음과 같습니다.

  • 메소드를 통해 변경되는 값 유형 변수가 일반 로컬이면 정상적으로 변경됩니다.

  • but if it is a hoisted local (because it's a closed-over variable of an anonymous function or in an iterator block) then the local is actually generated as a read-only field, and the gear that ensures that mutations happen on a copy takes over.

Unfortunately the spec provides little guidance on this matter. Clearly something is broken because we're doing it inconsistently, but what the right thing to do is not at all clear.


Struct methods are inlined when type of struct is known at compile time, and calling method via interface is slow, so answer is: because of performance reason.

ReferenceURL : https://stackoverflow.com/questions/3168311/why-do-bcl-collections-use-struct-enumerators-not-classes

반응형