앞서 단일 객체의 같음에 대해서 테스트해봤다.
[🌍 C# Study/C# 케이스 스터디] - C# 클래스, 구조체, 레코드의 같음
하지만 위에 글에서는 단일 객체끼리의 같음만을 비교한다.
List, Array 등 여러 요소들이 담겨있는 시퀀스 컨테이너들의 같음은 어떻게 판단할 것인가?
개별 요소들의 순서와 값이 모두 일치해야 같다고 할 수 있을 것이다.
이를 위해 C#라이브러리에서는 Enumerable.SequenceEqual 메서드를 제공한다.
MSDN : Enumerable.SequenceEqual 메서드
https://learn.microsoft.com/ko-kr/dotnet/api/system.linq.enumerable.sequenceequal?view=net-8.0
오버로딩으로 두 가지 버전을 제공하며, 비교할 시퀀스들을 제공하고 IEqualityComparer를 직접 넘겨줄 수도 있다.
두 시퀀스의 길이가 같고, 내용도 같다면 true를 반환한다. 단. 둘 중 하나가 null일 경우 예외를 반환한다.
둘 중 하나 이상이 null인 경우의 처리는 별도로 구현해야 된다는 이야기이다. 둘 다 null일 경우 같다고 할 수 있을까? 이는 구현 의도에 따라 다르게 처리해야 될 것이다.
SequenceEqual<TSource>(IEnumerable<TSource>, IEnumerable<TSource>);
SequenceEqual<TSource>(IEnumerable<TSource>, IEnumerable<TSource>, IEqualityComparer<TSource>);
1. 비교 테스트 : 별도 구현 없이 바로 비교
다시 데이터 클래스를 준비하자.
public class MyData
{
public int num1;
public int num2;
public MyData(int num1, int num2)
{
this.num1 = num1;
this.num2 = num2;
}
}
테스트는 다음과 같다.
public class EqualTest : MonoBehaviour
{
[Button("테스트2번")]
public void Test2()
{
var listA = new List<MyData>()
{
new MyData(1, 2),
new MyData(2, 3),
};
var listB = new List<MyData>()
{
new MyData(1, 2),
new MyData(2, 3),
};
Debug.Log("---------테스트------------");
Debug.Log("== : " + (listA == listB));
Debug.Log("objectEquals : " + (listA.Equals(listB)));
Debug.Log("Enumerable.Equals : " + Enumerable.Equals(listA, listB));
Debug.Log("Enumerable.SequenceEqual : " + Enumerable.SequenceEqual(listA, listB));
}
}
== 와 object.Equals, Enumerable.Equals는 두 리스트의 레퍼런스를 비교하니 false를 반환한다.
Enumerable.SequenceEqual 은 요소의 길이와 순서, 대응되는 요소가 같아야 하는데 대응되는 요소를 비교할 때 레퍼런스 비교를 하게 되니 false를 반환한다.
때문에 record 타입이나 struct 타입으로 실행해 보면 다음과 같이 Enumerable.SequenceEqual이 true를 반환하게 된다.
2. IEquatable<T>를 구현한 상태에서 비교
그렇다면 클래스일 때도 각 요소를 구조 같음으로 비교할 수 있게 IEquatable<T>를 구현해보면 어떨까?
public class MyData : IEquatable<MyData>
{
public int num1;
public int num2;
public MyData(int num1, int num2)
{
this.num1 = num1;
this.num2 = num2;
}
public bool Equals(MyData other)
{
if (other == null) return false;
return (this.num1 == other.num1) && (this.num2 == other.num2);
}
}
다음의 결과를 얻을 수 있다.
IEquatable<T>를 구현하면 직접 정의한 비교를 통해 같음을 체크할 수 있다!
이번에는 null인 경우 처리가 잘 되었는지 테스트해보자.
var listA = new List<MyData>()
{
null,
new MyData(2, 3),
};
var listB = new List<MyData>()
{
null,
new MyData(2, 3),
};
1. 한쪽이 null인 경우 null 처리했던 Equals내부의 첫 번째 줄에서 바로 false를 반환한다.
2. 양쪽이 null 인 경우 object.Euqals의 기본 정의에 의해 true가 반환된다.
때문에 Enumerable.SequenceEqual 비교를 할 때 null이 섞여있어도 오류가 나지 않게 된다.
3. string의 비교
마지막으로 데이터 객체 대신 레퍼런스 타입이지만 특수하게 비교되는 문자열로 테스트해 보자.
public void Test2()
{
var listA = new List<string>()
{
null,
"abcd",
};
var listB = new List<string>()
{
null,
"abcd",
};
Debug.Log("---------테스트------------");
Debug.Log("Enumerable.SequenceEqual : " + Enumerable.SequenceEqual(listA, listB));
}
문자열은 문자가 같다면 같은 레퍼런스를 사용하게 되니 true를 반환한다.
그렇다면 대소문자만 다른 경우는 어떨까?
var listA = new List<string>()
{
null,
"abcd",
};
var listB = new List<string>()
{
null,
"ABcd",
};
일반 문자열을 ==나 Equals로 비교할 때와 마찬가지로 false를 반환한다.
string에서는 문자열의 대소문자를 무시하기 위해 stringComparison을 사용하곤 했다.
bool isSame = "abcd".Equals("ABcd", StringComparison.OrdinalIgnoreCase);
Sequence.Equal의 두 번째 오버로딩에서는 별도의 Comparer를 넘길 수 있게 구현되어 있다.
따라서 다음과 같이 StringComparer를 사용하면 대소문자만 다른 경우 같은 문자열로 취급할 수 있다.
Enumerable.SequenceEqual(listA, listB, StringComparer.OrdinalIgnoreCase);
즉, 객체 안에 Equals 등을 구현하기 힘들거나 클래스의 간결화를 위해 비교로직을 분리하고 싶다면 별도의 커스텀 Comparer 클래스를 정의해서 활용하면 된다.
결론
Enumerable.SeuqneceEqual을 알게 된 후로는 꽤나 즐겨 사용하고 있다. 하지만 데이터 객체의 멤버변수가 추가될 때마다 Euqals에서도 변경을 반영해줘야 하는데 이로 인한 휴먼에러가 자주 발생하는 편이다.
때문에 Equals를 오버라이딩한다면 언제나 주의 또 주의해야 한다!
'🌍 C# Study > C# 케이스 스터디' 카테고리의 다른 글
유니티 마우스가 특정 영역에 있는지 체크 (0) | 2024.11.07 |
---|---|
C# 클래스, 구조체, 레코드의 같음 (0) | 2024.05.27 |
소수의 판별 (0) | 2023.01.25 |
약수의 개수 (0) | 2023.01.25 |
C# LINQ GroupBy, Group by into (0) | 2023.01.07 |