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개의 좋아요