C# 행을 추가하고 조건에 따라 삭제하는 코드

안녕하세요. :smile:

C#에서 제가 구현한 로직에 대한 질문입니다.

목록에서 중복이 아닌 데이터를 목록에 추가하고 목록의 개수가 설정치에 도달하면 첫번째 배열을 제거하는 방식입니다.

아래와 유사한 코드를 자주 만들게 되는데요. 이것을 개선하거나 더 좋고 다양한 방식으로 접해보고 싶습니다.

public ObservableCollection<string> Colors { get; }

private void AddColor(string color)
{
    if (!Colors.Contains(color))
    {
        Colors.Add(color);
    }
    if (Colors.Count == 200)
    {   
        Colors.RemoveAt(0);
    }
}

그리고 이런 종류의 로직을 부르는 명칭이 혹시 있다면 알고 싶습니다. :smile:

읽어주셔서 감사합니다.

좋아요 3

먼저 개념적으로는 특정개수만 보유하고 있다는 것으로 버퍼라고 할 수 있겠고요, 선입선출 하므로 큐의 구조도 가지고 있다고 볼 수 있습니다.

이런 코드를 디자인 할 땐 코드가 자주 호출되어 성능 개선을 해야 하는가? 그렇지 않으면 읽기 쉽고 짧은 코드로 작성하는 원칙으로 하면 될 것 같습니다.

특정 색 목록을 편집하기 위한 목적이고, 그 개수를 200개로 제한하는 색 추가 함수이므로, 성능보다는 간결한 코딩에 부합되어 보이고요,

그런 목적으로는 코드가 깔끔하다고 생각합니다.

좋아요 3

저는 개인적으로 이런 부분은 Model에 들어가야하지 않나 생각됩니다.
컬렉션을 class로 한 번 더 감싸는 것인데요. 퍼포먼스 상에는 많이 차이는 없더라도, 소프트웨어를 보다 구조적으로 만들 수 있기 때문에 그렇게 생각합니다.

viewmodel에 프로퍼티와 메서드. 커맨드를 많이 노출하면 가독성이 떨어지기도 하고요 ㅎㅎ

그래서 제 견해로는 컬렉션과 add color를 하나의 새로운 클래스로 정의하셔서 쓰시면 좋을 거 같습니다.

저도 실제로 그렇게 사용하고 있습니다.

좋아요 4

저라면 Queue<T> 를 상속받아 ObservableMaxQueue<T> 라는 클래스를 정의하고 INotifyCollectionChangedINotifyPropertyChanged 인터페이스를 구현할 것 같습니다. 그러면 Max 값을 유연하게 정의할 수도 있고 Add 할 때 개수를 확인하고 삭제를 해도 되고요. 현재 코드 구조를 최대한 유지하면서, 선입선출이어야 하는 자료구조도 활용하고, 비슷한 코드에 재사용하기 수월할 거 같습니다. 즉 중복되는 부분을 새롭게 만들 클래스 내부로 옮기는거죠.

우려되는 부분은 INotifyCollectionChanged 을 구현하는 부분인데, 꽤 난해합니다… NotifyCollectionChangedAction 도 이해를 하셔야 하고 NewStartingIndex 를 어디로 잡아야 하는지 등이 복잡해서 ObservableCollection<T> 소스 코드를 살펴보시고 토이 프로젝트로 동작 원리를 검토한 뒤 테스트 코드를 작성하길 추천드립니다.

좋아요 3

@김예건 좋은 설명 감사드립니다. :smile:

간단한 샘플도 만들어주시면 엄청 도움 될 것 같아요!!
다시한번 감사드립니다. :smile:

좋아요 1

@Vincent 답변 감사드립니다. :smile:

엄청 공감되네요. ㅋ

좋아요 1

목록에서 중복이 아닌 데이터를 목록에 추가하고 목록의 개수가 설정치에 도달하면 첫번째 배열을 제거하는 방식입니다.

이번에 저희 회사 신입이 작성한 도구와 비슷한데요.
FixedSet 정도로 명명한 개체를 만들어 사용하는것을 추천합니다.

좋아요 2

일단 아래처럼 만들어 보았고, 질문있으시면 댓글달아주세요.

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Collections.Specialized;
using System.Runtime.CompilerServices;

namespace YegunKim
{
    /// <summary>
    /// Observable 하면서 Max 가 정해진 Queue 입니다.
    /// </summary>
    /// <typeparam name="T">Queue 가 사용할 데이터 타입</typeparam>
    /// <see cref="https://referencesource.microsoft.com/#system/compmod/system/collections/generic/queue.cs"/>
    /// <see cref="https://referencesource.microsoft.com/#system/compmod/system/collections/objectmodel/observablecollection.cs"/>
    public class ObservableMaxQueue<T> : Queue<T>, INotifyPropertyChanged, INotifyCollectionChanged
    {
        private readonly int _capacity;

        #region INotifyPropertyChanged
        // MVVM 패턴에서 정상적으로 동작하려면 반드시 필요한 이벤트입니다.
        public event PropertyChangedEventHandler PropertyChanged;
        private void OnPropertyChanged([CallerMemberName] string name = null)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
        }
        #endregion

        #region INotifyCollectionChanged
        public event NotifyCollectionChangedEventHandler CollectionChanged;
        protected virtual void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
        {
            CollectionChanged?.Invoke(this, e);
        }
        private void OnCollectionChanged(NotifyCollectionChangedAction action, object item, int index)
        {
            OnCollectionChanged(new NotifyCollectionChangedEventArgs(action, item, index));
        }
        private void OnCollectionChanged(NotifyCollectionChangedAction action, object oldItem, object newItem, int index)
        {
            OnCollectionChanged(new NotifyCollectionChangedEventArgs(action, newItem, oldItem, index));
        }
        private void OnCollectionReset()
        {
            OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
        }
        #endregion

        public ObservableMaxQueue(int capacity) : base(capacity)
        {
            _capacity = capacity;
        }

        public bool TryEnqueue(T item)
        {
            try
            {
                Enqueue(item);
            }
            catch (InvalidOperationException)
            {
                return false;
            }
                
            return true;
        }

        // Queue<T> 의 Enqueue 는 virtual 이 아니라 상속할 수 없지만, 아래처럼 base 와 new 를 사용하여 속이는 건 가능합니다.
        public new void Enqueue(T item)
        {
            if (Contains(item))
                throw new InvalidOperationException($"{nameof(ObservableMaxQueue<T>)} already contains {item}.");
                
            if(_capacity < Count + 1)
                throw new InvalidOperationException($"{nameof(ObservableMaxQueue<T>)} is already full.");

            base.Enqueue(item);
            OnPropertyChanged(nameof(Count));
            // 여기서 애매한게 index 인데, Queue 소스코드를 보면 _array[_tail] 에 삽입하니 Count 로 설정했습니다. 
            OnCollectionChanged(NotifyCollectionChangedAction.Add, item, Count);
        }

        public new T Dequeue()
        {
            var item = base.Dequeue();
            OnPropertyChanged(nameof(Count));
            // Enqueue 와 마찬가지로 Queue 소스코드를 보면 _array[_head] 에서 꺼내니 0 으로 설정했습니다.
            OnCollectionChanged(NotifyCollectionChangedAction.Remove, item, 0);
            return item;
        }

        public new void Clear()
        {
            base.Clear();
            OnCollectionReset();
        }

        public new void CopyTo(T[] array, int arrayIndex)
        {
            throw new NotImplementedException("다른 메서드를 참고해서 구현하시면 됩니다.");
        }
    }
}

아래처럼 테스트 코드도 작성했고 테스트도 통과했습니다.

using NUnit.Framework;
using System;
using System.Collections.Specialized;
using System.ComponentModel;
using YegunKim;

namespace Test
{
    public class ObservableMaxQueueUnitTests
    {
        [Test]
        public void TestBasic()
        {
            var queue = new ObservableMaxQueue<int>(5);
            queue.Enqueue(1);
            queue.Enqueue(2);
            queue.Enqueue(3);
            queue.Enqueue(4);
            queue.Enqueue(5);
            Assert.AreEqual(5, queue.Count);

            Assert.AreEqual(1, queue.Dequeue());
            Assert.AreEqual(2, queue.Dequeue());
            Assert.AreEqual(3, queue.Dequeue());
            Assert.AreEqual(4, queue.Dequeue());
            Assert.AreEqual(5, queue.Dequeue());

            Assert.AreEqual(0, queue.Count);
        }

        [Test]
        public void TestError()
        {
            var queue = new ObservableMaxQueue<int>(5);
            queue.Enqueue(1);
            queue.Enqueue(2);
            queue.Enqueue(3);
            queue.Enqueue(4);
            queue.Enqueue(5);

            Assert.Throws<InvalidOperationException>(() => queue.Enqueue(6));
            Assert.AreEqual(5, queue.Count);

            Assert.False(queue.TryEnqueue(6));
            Assert.AreEqual(5, queue.Count);

            Assert.AreEqual(1, queue.Dequeue());
        }

        [Test]
        public void TestPropertyChanged()
        {
            var queue = new ObservableMaxQueue<int>(5);
            var history = AssertExtensions.Raises<PropertyChangedEventArgs>(queue, nameof(queue.PropertyChanged), () =>
            {
                queue.Enqueue(1);
            });

            Assert.AreEqual(1, history.Count);
            Assert.AreEqual(queue, history[0].Sender);
            Assert.AreEqual(nameof(history.Count), history[0].EventArgs.PropertyName);
        }

        [Test]
        public void TestCollectionChanged()
        {
            var queue = new ObservableMaxQueue<int>(5);
            var history = AssertExtensions.Raises<NotifyCollectionChangedEventArgs>(queue, nameof(queue.CollectionChanged), () =>
            {
                queue.Enqueue(1);
            });

            Assert.AreEqual(1, history.Count);
            Assert.AreEqual(queue, history[0].Sender);
            Assert.AreEqual(NotifyCollectionChangedAction.Add, history[0].EventArgs.Action);
            Assert.NotNull(history[0].EventArgs.NewItems);
            Assert.AreEqual(1, history[0].EventArgs.NewItems.Count);
            Assert.AreEqual(1, history[0].EventArgs.NewItems[0]);

            history = AssertExtensions.Raises<NotifyCollectionChangedEventArgs>(queue, nameof(queue.CollectionChanged), () =>
            {
                queue.Dequeue();
            });

            Assert.AreEqual(1, history.Count);
            Assert.AreEqual(queue, history[0].Sender);
            Assert.AreEqual(NotifyCollectionChangedAction.Remove, history[0].EventArgs.Action);
            Assert.NotNull(history[0].EventArgs.OldItems);
            Assert.AreEqual(1, history[0].EventArgs.OldItems.Count);
            Assert.AreEqual(1, history[0].EventArgs.OldItems[0]);
        }
    }
}

MVVM 패턴에서도 잘 동작하는지는 테스트를 하지 못해서… 해보시고 문제있으시면 알려주세요. 1시간 만에 작성하고 테스트한 코드라서 빠트린 부분이 있을 수 있습니다.

P.S @SangHyeon.Kim 님이 제시하신 것처럼 중복이 안된다는 의미로 클래스 이름에 Set 이나 Unique 을 포함시켜야 하지 않나 고민되네요…

좋아요 4

오우 상세한 예제네요. ㅎㅎㅎㅎㅎㅎㅎ

좋아요 3

@김예건 앗 알려주신 부분 잘 보고 후기도 남겨볼께요.

테스트까지 해주셔서 정말 감사합니다. :smile:

좋아요 2