그들이 말하는 것처럼, 악마는 세부 사항에 있습니다 ...
컬렉션 열거의 두 가지 방법의 가장 큰 차이점 foreach
은 상태 를 전달하는 반면 ForEach(x => { })
그렇지는 않습니다.
그러나 결정에 영향을 줄 수있는 몇 가지 사항이 있고 두 경우 모두를 코딩 할 때주의해야 할 사항이 있기 때문에 좀 더 깊이 파고 들겠습니다.
사용할 수 있습니다 List<T>
행동을 관찰하기 위해 우리의 작은 실험에서. 이 실험에서는 .NET 4.7.2를 사용하고 있습니다.
var names = new List<string>
{
"Henry",
"Shirley",
"Ann",
"Peter",
"Nancy"
};
foreach
먼저 이것을 반복합니다 :
foreach (var name in names)
{
Console.WriteLine(name);
}
이를 다음과 같이 확장 할 수 있습니다.
using (var enumerator = names.GetEnumerator())
{
}
열거자를 손에 넣은 다음 덮개를 살펴보십시오.
public List<T>.Enumerator GetEnumerator()
{
return new List<T>.Enumerator(this);
}
internal Enumerator(List<T> list)
{
this.list = list;
this.index = 0;
this.version = list._version;
this.current = default (T);
}
public bool MoveNext()
{
List<T> list = this.list;
if (this.version != list._version || (uint) this.index >= (uint) list._size)
return this.MoveNextRare();
this.current = list._items[this.index];
++this.index;
return true;
}
object IEnumerator.Current
{
{
if (this.index == 0 || this.index == this.list._size + 1)
ThrowHelper.ThrowInvalidOperationException(ExceptionResource.InvalidOperation_EnumOpCantHappen);
return (object) this.Current;
}
}
두 가지가 즉시 명백해진다 :
- 기본 컬렉션에 대한 친밀한 지식을 갖춘 상태 저장 객체가 반환됩니다.
- 컬렉션의 복사본은 얕은 복사본입니다.
이것은 물론 스레드 안전이 아닙니다. 위에서 지적했듯이 반복하는 동안 컬렉션을 변경하는 것은 나쁜 모조입니다.
그러나 반복하는 동안 컬렉션을 모으는 외부의 수단을 통해 반복하는 동안 컬렉션이 무효화되는 문제는 어떻습니까? 모범 사례에서는 작업 및 반복 중에 컬렉션의 버전을 관리하고 기본 컬렉션이 변경되는시기를 감지하기 위해 버전을 확인하는 것이 좋습니다.
여기는 정말 어두운 곳입니다. Microsoft 설명서에 따르면 :
요소 추가, 수정 또는 삭제와 같이 컬렉션이 변경되면 열거 자의 동작이 정의되지 않습니다.
그게 무슨 뜻이야? 예를 들어, List<T>
예외 처리를 구현한다고해서 구현하는 모든 컬렉션이 IList<T>
동일하게 수행되는 것은 아닙니다 . 이는 Liskov 대체 원칙을 명백히 위반 한 것으로 보입니다.
수퍼 클래스의 객체는 응용 프로그램을 중단하지 않고 서브 클래스의 객체로 대체 가능해야합니다.
또 다른 문제는 열거자가 구현해야한다는 것 IDisposable
입니다. 즉, 호출자가 잘못했을 때뿐만 아니라 작성자가 Dispose
패턴을 올바르게 구현하지 않은 경우 잠재적 인 메모리 누수의 또 다른 원인이 됩니다.
마지막으로, 우리는 평생 문제가 있습니다 ... 반복자가 유효하지만 기본 컬렉션이 사라지면 어떻게됩니까? 우리는 지금의 스냅 샷 무엇인지는 당신이 수집하고 반복자의 수명을 분리 할 때 ..., 당신은 문제가 요구된다.
이제 살펴 보자 ForEach(x => { })
:
names.ForEach(name =>
{
});
이것은 다음과 같이 확장됩니다.
public void ForEach(Action<T> action)
{
if (action == null)
ThrowHelper.ThrowArgumentNullException(ExceptionArgument.match);
int version = this._version;
for (int index = 0; index < this._size && (version == this._version || !BinaryCompatibility.TargetsAtLeast_Desktop_V4_5); ++index)
action(this._items[index]);
if (version == this._version || !BinaryCompatibility.TargetsAtLeast_Desktop_V4_5)
return;
ThrowHelper.ThrowInvalidOperationException(ExceptionResource.InvalidOperation_EnumFailedVersion);
}
중요한 사항은 다음과 같습니다.
for (int index = 0; index < this._size && ... ; ++index)
action(this._items[index]);
이 코드는 열거자를 할당하지 않으며 ( Dispose
)에 반복 하지 않고 일시 중지 하지 않습니다 .
이것은 또한 기본 콜렉션의 단순 사본을 수행하지만 콜렉션은 이제 스냅 샷입니다. 작성자가 컬렉션 변경 또는 'stale'에 대한 검사를 올바르게 구현하지 않은 경우 스냅 샷은 여전히 유효합니다.
이것은 어떤 식 으로든 평생 문제의 문제로부터 당신을 보호하지 않습니다 ... 기본 컬렉션이 사라지면 이제는 무엇을 가리키는 얕은 사본이 있지만 적어도 Dispose
문제 는 없습니다. 고아 반복자 다루기 ...
그렇습니다. 반복자는 ... 때때로 상태를 갖는 것이 유리하다고 말했습니다. 데이터베이스 커서와 비슷한 것을 유지하고 싶다고 가정합시다. 아마도 여러 foreach
스타일 Iterator<T>
이 갈 길입니다. 나는 평생 문제가 너무 많기 때문에 개인적 으로이 스타일의 디자인을 싫어하고, 당신이 믿고있는 컬렉션의 저자의 은혜에 의존합니다 (문자 그대로 모든 것을 처음부터 작성하지 않는 한).
항상 세 번째 옵션이 있습니다 ...
for (var i = 0; i < names.Count; i++)
{
Console.WriteLine(names[i]);
}
섹시하지는 않지만 치아가 있습니다 ( 톰 크루즈 와 영화 The Firm 에게 사과 )
그것은 당신의 선택이지만, 지금 당신은 알고 있으며 정보를 얻을 수 있습니다.