시작
문득 ObservableCollection에서
ObservableCollection 객체를 대충 할당해놓고, 사후에 반복문을 통해 CollectionItem을 초기화하는 게 나을지, 아니면 ObservableCollection(List list) 생성자를 통해 한번에 생성자에 이미 List 객체를 초기화 하는게 나은지 궁금했습니다.
뭐 성능이라는 것이 누적되어서 체감이 되어야 유의미한 성능이라고 불릴 수 있고, 체감도 안되는 미세한 차이는 성능이라고 말하기 애매한 부분이겠지만 정확하게 알고 싶었습니다.
먼저 ChatGPT-4에게 물어봤습니다.
ChatGPT 질의응답
Q. 그렇다면 ObservableCollection() 생성자를 생성하고 하나씩 등록하는 게 좋을까, 아니면 ObservableCollection(new List()) 형태로 등록하는게 좋을까?
어느 쪽이 Notify가 덜 발생해?
A. ObservableCollection<T>
생성자를 사용하여 컬렉션을 생성한 후 항목을 하나씩 추가하는 방법과 ObservableCollection<T>(new List<T>())
형태로 한 번에 등록하는 방법 중 후자가 CollectionChanged 이벤트 알림이 덜 발생합니다.
하나씩 추가하는 경우, 항목이 추가될 때마다 CollectionChanged 이벤트 알림이 발생합니다. 반면, ObservableCollection<T>(new List<T>())
형태로 등록하는 경우, 컬렉션에 대한 초기 설정이 완료된 후에만 CollectionChanged 이벤트가 한 번 발생합니다.
따라서 초기 설정 시 이벤트 알림이 최소화되는 것이 선호되는 경우, ObservableCollection<T>(new List<T>())
형태로 등록하는 것이 더 좋습니다.
예를 들어 인코딩 목록을 ObservableCollection으로 변환하는 경우 다음과 같이 할 수 있습니다:
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Text;
public class EncodingList
{
public static ObservableCollection<Encoding> GetEncodings()
{
return new ObservableCollection<Encoding>(new List<Encoding>
{
Encoding.UTF8,
Encoding.UTF7,
Encoding.UTF32,
Encoding.Unicode,
Encoding.ASCII,
Encoding.BigEndianUnicode,
// 여기에 추가로 사용할 인코딩을 넣을 수 있습니다.
});
}
}
이렇게 하면 인코딩 목록이 한 번에 등록되고 CollectionChanged 이벤트 알림이 최소화됩니다.
확인
ChatGPT-4에게 낚시를 많이 당해봤기 때문에 지식에 대한 검증이 필요했습니다.
정말 그런지.
이 밑의 소스는 모두 .NET Reference Source에 있습니다.
우리의 ObservableCollection는 아래와 같이 생겼습니다.
public class ObservableCollection<T> : Collection<T>, INotifyCollectionChanged, INotifyPropertyChanged
그리고 아래와 같은 생성자 목록을 갖고 있습니다.
public ObservableCollection() : base() { }
public ObservableCollection(List<T> list) : base((list != null) ? new List<T>(list. Count) : list)
{
CopyFrom(list);
}
public ObservableCollection(IEnumerable<T> collection)
{
if (collection == null)
throw new ArgumentNullException("collection");
CopyFrom(collection);
}
private void CopyFrom(IEnumerable<T> collection)
{
IList<T> items = Items;
if (collection != null && items != null)
{
using (IEnumerator<T> enumerator = collection.GetEnumerator())
{
while (enumerator.MoveNext())
{
items.Add(enumerator.Current);
}
}
}
}
CopyFrom 메서드를 보면
IList<T> items = Items;
라는 코드가 있고 밑에서
while (enumerator.MoveNext())
{
items.Add(enumerator. Current);
}
를 하고 있었습니다.
여기서 메서드 내의 지역변수인 items 말고 Items는 상속받고 있는 Class인 Collection의 Property 였습니다.
[Serializable]
[System.Runtime.InteropServices.ComVisible(false)]
[DebuggerTypeProxy(typeof(Mscorlib_CollectionDebugView<>))]
[DebuggerDisplay("Count = {Count}")]
public class Collection<T>: IList<T>, IList, IReadOnlyList<T>
{
IList<T> items;
protected IList<T> Items {
get { return items; }
}
}
또한 Add 메서드도 Collection 안에 있는 메서드 였습니다.
public void Add(T item) {
if( items.IsReadOnly) {
ThrowHelper.ThrowNotSupportedException(ExceptionResource.NotSupported_ReadOnlyCollection);
}
int index = items. Count;
InsertItem(index, item);
}
우리가 ObservableCollection클래스에서 Add 메서드를 호출하는 것은 ObservableCollection 클래스의 메서드가 아닌 Collection 클래스의 메서드 였습니다.
그리고 그 Collection 클래스의 Add 메서드에는 아래 메서드를 추가로 호출하고 있습니다.
InsertItem(index, item);
이 메서드를 따라가 보겠습니다.
protected virtual void InsertItem(int index, T item) {
items.Insert(index, item);
}
InsertItem은 virtual 메서드였네요.
그렇다는 것은 재정의가 가능하다는 뜻이죠.
ObservableCollection 클래스에도 InsertItem이 있는지 확인해보겠습니다.
protected override void InsertItem(int index, T item)
{
CheckReentrancy();
base.InsertItem(index, item);
OnPropertyChanged(CountString);
OnPropertyChanged(IndexerName);
OnCollectionChanged(NotifyCollectionChangedAction.Add, item, index);
}
역시 있었네요. base.InsertItem(index, item)
을 통해 기존 Collection 클래스의 Items에 개체를 추가하고 ObservableCollection의 책임을 다하기 위한 Notify를 여기서 하고 있었네요.
결론
Notify는 InsertItem 메서드에서 하고 있었고,
InsertItem 메서드는 protected 메서드라서 사용자가 직접 호출이 불가하고,
Collection 클래스의 Add 메서드를 통해 재정의된 InsertItem 메서드가 호출되고 있었고,
ObservableCollection<T>(List<T> list)
생성자의 CopyForm 메서드에서는 Collection 클래스의 Add 메서드를 호출하고 있었으므로,
아무튼 ObservableCollection 클래스에서 Add 메서드를 호출하면 ObservableCollectionn의 InsertItem 메서드를 타게 됩니다.
즉,
public ObservableCollection<T> TestABC = new (new List<T>
{
new T(),
new T(),
new T(),
new T(),
});
와
public ObservableCollection<T> TestABC = new ();
TestABC.Add(new T());
TestABC.Add(new T());
TestABC.Add(new T());
TestABC.Add(new T());
는 Notify가 같은 횟수로 발생하며 ChatGPT가 거짓말을 했다는 결론이 나옵니다.