HashSet<T>

HashSet<T>

μš”μ¦˜ 많이 μ‚¬μš©ν•˜κ³  μžˆλŠ” μ»¬λ ‰μ…˜μœΌλ‘œ ν™œμš© κ°€μΉ˜μ— λŒ€ν•΄ κ³΅μœ ν•˜κ³ μž ν•©λ‹ˆλ‹€.

문제 상황


record Dispatch(string Title, DateOnly From, DateOnly To);

class Assignments
{
   // ... 
   
   public List<Dispatch> Dispatches{ get; init;} = [];
}

β€œνŒŒκ²¬(Dispatch)” μ΄λΌλŠ” 도메인 λ¬Έμ œλŠ” "κΈ°κ°„μ˜ 쀑볡이 λΆˆκ°€ν•˜λ‹€"λŠ” μš”κ΅¬ 사항이 μžˆμŠ΅λ‹ˆλ‹€.

List<T> λŠ” μ΄λŸ¬ν•œ μš”κ΅¬ 사항을 λ§Œμ‘±ν•˜κΈ°μ—λŠ” λΆ€μ‘±ν•©λ‹ˆλ‹€. λ§Œμ•½ μœ„ μ½”λ“œλ₯Ό κ·ΈλŒ€λ‘œ μ‚¬μš©ν•œλ‹€λ©΄, μ•„λž˜μ™€ 같은 λΆ€μ‚°μŠ€λŸ° μ½”λ“œκ°€ ν”„λ‘œμ νŠΈ 사방에 흩뿌렀질 κ²ƒμž…λ‹ˆλ‹€.

void Add(Dispatch dispach, Assignments on)
{
   var overlapped = on.Dispatches
      .Any(d => !(dispatch.To < d.From || d.To < dispatch.From));

   if (!overlapped)
   {
      on.Dispatches.Add(dispatch);
   }
}

ν•΄κ²°

이 문제λ₯Ό ν•΄κ²°ν•˜κΈ° μœ„ν•΄μ„œ 선택할 수 μžˆλŠ” μˆ˜λ‹¨μ€:

  1. 별도 자료 ꡬ쑰 μ •μ˜.
  2. κΈ°λ³Έ 자료 ꡬ쑰 νŒŒμƒ

μž…λ‹ˆλ‹€. 이 κΈ€μ—μ„œλŠ” λ‘λ²ˆμ§Έλ₯Ό μ„ νƒν•΄μ„œ HashSet<T> 을 νŒŒμƒν•©λ‹ˆλ‹€.

class DispatchSet : HashSet<Dispatch>
{
   public DispatchSet() : base(
      EqualityComparer<Dispatch>.Create(
      (x, y) => { 
          // 겹치면 true, μ•„λ‹ˆλ©΄ false 
      }, 
      d => 0 // 항상 같은 ν•΄μ‹œμ½”λ“œ λ°˜ν™˜
      )) 
   { }
}

λΆ€λͺ¨μ˜ μƒμ„±μžμ— "기간이 겹치면 true λ₯Ό λ°˜ν™˜"ν•˜λŠ” IEqualityComparer<Dispatch> λ₯Ό 맀개 λ³€μˆ˜λ‘œ λ„˜κΈ°λ„λ‘ μ •μ˜ν–ˆμŠ΅λ‹ˆλ‹€.

이 λΉ„κ΅κΈ°λŠ” Equals(x, y)의 호좜이 κ°•μ œλ˜λŠ” ꡬ쑰둜 일반적인 ν•΄μ‹œμ…‹( O(1) ) λ³΄λ‹€λŠ” μ„±λŠ₯ λΉ„νš¨μœ¨μ ( O(n) )이긴 ν•˜μ§€λ§Œ, μ΄λŠ” β€œκΈ°κ°„μ΄ κ²ΉμΉ˜μ§€ μ•Šμ•„μ•Ό ν•œλ‹€β€ λŠ” 도메인 μš”κ΅¬ 사항을 μ§€ν‚€κΈ° μœ„ν•œ λΉ„μš©μž…λ‹ˆλ‹€.

μ£Όμ„μ˜ κ΅¬ν˜„μ„ λ§€λ„λŸ½κ²Œ ν•˜κΈ° μœ„ν•΄, Dispatch 의 β€œλ°μ΄ν„° 응집” 문제λ₯Ό λ¨Όμ € ν•΄κ²°ν•©λ‹ˆλ‹€.

// record Dispatch(string Title, DateOnly From, DateOnly To);

record Dispatch(string Title, DateSpan Span);

record DateSpan(DateOnly From, DateOnly To)
{
   // From <= To λ₯Ό κ°•μ œ.
   public DateOnly To { get; init; } = From > To ? From : To;
}

static class DateSpanDefaults
{
   public static bool Overlapped(this DateSpan a, DateSpan b) =>
      !(b.To < a.From || a.To < b.From);
}

이 λ°‘μž‘μ—…μ„ λ°”νƒ•μœΌλ‘œ DispatchSet을 μ™„μ„±ν•©λ‹ˆλ‹€.

class DispatchSet() 
   : HashSet<Dispatch>(EqualityComparer<Dispatch>.Create(
      (x, y) => { 
         if (ReferenceEquals(x, y)) return true;
         if (x == null || y == null) return false;

         // 겹치면 true, μ•„λ‹ˆλ©΄ false 
         return x.Span.Overlapped(y.Span);
      }, d => 0));

이 μ»¬λ ‰μ…˜μ„ μ±„νƒν•œ Assignments μž…λ‹ˆλ‹€.

class Assignments
{
   // ... 
   // public List<Dispatch> Dispatches { get; } = [];   
   // public HashSet<Dispatch> Dispatches { get; } = [];   
   public DispatchSet Dispatches { get; } = [];
}

μ†ŒλΉ„ μ½”λ“œλ₯Ό 보자면,

void Add(Dispatch dispach, Assignments on)
{
   // var overlapped = on.Dispatches
   //   .Any(d => !(dispatch.To < d.From || d.To < dispatch.From));

   // if (!overlapped)
   // {
   //    on.Dispatches.Add(dispatch);
   // }

   if (on.Dispatches.Add(dispatch) is false)
   {
      MessageBox.Alert("파견의 기간은 쀑볡될 수 μ—†μŠ΅λ‹ˆλ‹€.")
   }
}
void AddRange(IEnumerable<Dispatch> dispaches, Assignments on)
{
   var addeds = new List<Dispatch>();
   var iterations = 0;
   foreach(var d in dispatches)
   {
      if (on.Dispatches.Add(d)) addeds.Add(d);
      iterations ++;
   }
  
   if (addeds.Count != iterations)
   {
      var overlaps = iterations - addeds.Count;
      var ok = MessageBox.Confirm(
         $"총 {iterations} 개 쀑에, 기간이 μ€‘λ³΅λœ " +
         $"{overlaps} 개λ₯Ό λΉΌκ³  μ €μž₯ν• κΉŒμš”?")
      if (!ok) RemoveRange(addeds, on);
   }
}
var d1 = new Dispatch("λΆ€μ‚° ν˜„μž₯", new(new(2026, 1,1), new(2026, 1,10)));
var d2 = new Dispatch("κ΄‘μ£Ό ν˜„μž₯" , new(new(2026, 1,12), new(2026, 1,20)));
var d3 = new Dispatch("μ œμ£Όλ„ ν˜„μž₯", new(new(2026, 1,15), new(2026, 1,25)));

DispatchSet  dispatches = [d1, d2, d3];
if (dispatches.Count != 3)
{
   foreach(var d in new[] {d1, d2, d3}.Except(dispatches))
      Console.WriteLine($"λ‚ μ§œκ°€ 겹쳐, {d.Title}은 μ œμ™Έλ˜μ—ˆμŠ΅λ‹ˆλ‹€.");
}

// κ²°κ³Ό
λ‚ μ§œκ°€ 겹쳐, μ œμ£Όλ„ ν˜„μž₯은 μ œμ™Έλ˜μ—ˆμŠ΅λ‹ˆλ‹€.

μ½”λ”© νš¨μœ¨μ„±

사싀 HashSet<T>(κ³Ό List<T>)λŠ” νŒŒμƒμ„ 염두해 λ‘” 객체가 μ•„λ‹ˆκΈ° λ•Œλ¬Έμ— μΌλ°˜μ μœΌλ‘œλŠ” κΆŒκ³ λ˜μ§€ μ•ŠλŠ” λ°©μ‹μž…λ‹ˆλ‹€.

ν•˜μ§€λ§Œ, DispatchSet 은 HashSet<T>의 인프라λ₯Ό μ•„λ¬΄λŸ° μˆ˜μ • 없이 κ·ΈλŒ€λ‘œ μ‚¬μš©ν•˜κ³  μžˆμ–΄, 이런 ꢌ고 사항을 μœ„λ°°ν•œλ‹€κ³  λ³Ό 수 μ—†κ³ , μˆ˜μ •μ΄ 없기에 EF Core 의 νŠΈλž™μ»€, System.Text.Json 직렬화 도ꡬ λ“±, λ‹·λ„· 기반 인프라 ν˜Έν™˜μ„±λ„ μžμ—°μŠ€λŸ½κ²Œ μœ μ§€λ˜λŠ” μž₯점이 μžˆμŠ΅λ‹ˆλ‹€.

자료ꡬ쑰λ₯Ό λ³„λ„λ‘œ μ •μ˜ν•˜λŠ” 경우, μ΄λŸ¬ν•œ ν˜Έν™˜μ„± λ¬Έμ œκΉŒμ§€ κ³ λ €ν•΄μ•Ό ν•΄μ„œ, μ „λ°˜μ μΈ μ½”λ”© νš¨μœ¨μ„±μ΄ μ’‹λ‹€κ³ λŠ” ν•  수 μ—†μŠ΅λ‹ˆλ‹€. (토큰도 많이 λ“€κ² μ£ .)

μ •λ¦¬ν•˜μžλ©΄

DispatchSet 은 κΈ°κ°„μ˜ 쀑볡을 ν—ˆμš©ν•˜μ§€ μ•Šμ•„μ•Ό ν•˜λŠ” 도메인 μš”κ΅¬ 사항을 μΆ©μ‘±ν•˜κΈ° μœ„ν•΄ μ •μ˜λ˜μ—ˆμŠ΅λ‹ˆλ‹€.

이 μš”κ΅¬ 사항을 μ’€ 더 μΌλ°˜ν™”ν•΄ 보면, "쀑볡을 ν—ˆμš©ν•˜μ§€ μ•ŠλŠ” μ»¬λ ‰μ…˜"에 λŒ€ν•œ μˆ˜μš”λΌκ³  ν•  수 μžˆλŠ”λ°, λ§Žμ€ 도메인 λ¬Έμ œμ—μ„œ κ³΅ν†΅μ μœΌλ‘œ 자주 λ°œμƒν•˜λŠ” νŽΈμž…λ‹ˆλ‹€.

μ΄λŸ¬ν•œ μˆ˜μš”λŠ” μ•„λž˜μ™€ 같은 λ‹¨μΆœν•œ μ½”λ“œ νŒ¨ν„΄μœΌλ‘œ μ‰½κ²Œ 해결이 κ°€λŠ₯ν•©λ‹ˆλ‹€.

public class FarSet() 
    : HashSet<Far>(IEqualityComparer<Far>);
8 Likes