[C#] LINQ와 FindAll()에 대한 고찰

0시0분·2024년 2월 9일
0

C#

목록 보기
6/9

LINQ (Language-Integrated Query)

공식 문서
https://learn.microsoft.com/ko-kr/dotnet/csharp/linq/

쿼리를 C#에서도 사용할수 있게 만들어진 기능이다.
원하는 데이터만 추출해낼수 있다는 장점이 있다.

기본 형식은 아래와 같이 사용한다.

List<string> testList = new List<string>{ "123", "1233", "12", "345" };
var linqList = from num in testList
               where num.Contains("1")
               select num;
foreach (string ll in linqList)
    Debug.Log($"@@ {ll}");



FindAll()

아주 비슷한 기능으로 List의 FindAll() 메소드가 있다.

공식 문서
https://learn.microsoft.com/ko-kr/dotnet/api/system.collections.generic.list-1.findall?view=net-8.0

람다식을 사용한다.

List<string> testList = new List<string>{ "123", "1233", "12", "345" };
var findList = testList.FindAll(test => test.Contains("1"));
foreach (string tt in findList)
    Debug.Log($"@@ {tt}");


출력되는 결과는 동일하다.

차이점

가장 큰 차이점은 두 결과 리스트의 타입이 IEnumerable형과 List형으로 다르다는 점이다.

정확히는 Linq의 Where과 Select메소드의 반환형이 IEnumerable<T> 형이다.

public static IEnumerable<TSource> Where<TSource>(this IEnumerable<TSource> source, Func<TSource, bool> predicate);
public static IEnumerable<TSource> Where<TSource>(this IEnumerable<TSource> source, Func<TSource, int, bool> predicate);
public static IEnumerable<TResult> Select<TSource, TResult>(this IEnumerable<TSource> source, Func<TSource, TResult> selector);
public static IEnumerable<TResult> Select<TSource, TResult>(this IEnumerable<TSource> source, Func<TSource, int, TResult> selector);

반환형이 동일하기 때문에 Where의 결과물로 Select를 해도 아무런 문제가 없다.

즉, FindAll()의 경우 무조건 List가 새로 생성이 된다.

public List<T> FindAll(Predicate<T> match);

그렇다면 IEnumerable로 반환된다는 것의 의미는 뭘까?
C# 컬렉션에 속하는 모든 클래스는 기본적으로 IEnumerable<T> 인터페이스를 가지고 있다.

👀 IEnumerable and IEnumerator
https://velog.io/@sivese/C-IEnumerable-and-IEnumerator

위 블로그에 자세히 설명이 되어있는것 같아 참고했다.

IEnumerable<T>란 일반적으로 foreach()문에서 요소들을 하나씩 꺼낼수 있는지,
즉 열거 가능한 객체인지를 나타내는 인터페이스라 할수 있다.

👀 IEnumerable vs ICollection vs IList 차이점
https://bigexecution.tistory.com/72

그러나 다르게 이야기 하자면, 정말 열거하는 기능밖에는 존재하지 않는다는 이야기가 된다.

FindAll()의 결과물은 삭제, 추가를 비롯한 컬렉션의 기능들을 대부분 사용할 수 있지만,
LINQ의 결과물은 해당 작업을 수행할 수가 없다.

어떤게 더 효율적일까

스택 오버플로우에서 관련 내용들을 찾아본 결과 Where()을 쓰는것이 훨씬 효율적이라는 의견이 많았다.

아래 두 글이 의견이 제일 많이 달려 있었는데,

Stack Overflow - Where() VS FindAll()
👀 https://stackoverflow.com/questions/1938204/linq-where-vs-findall
👀 https://stackoverflow.com/questions/2260220/c-sharp-findall-vs-where-speed?noredirect=1&lq=1

직접 테스트코드를 만들어서 시간을 측정해주신 분도 계시니 참고하면 좋을것 같다.

대충 결과물을 가지고 추가작업을 수행할때에는 List형식이, 단순 결과만 사용할때는 IEnumerable형이 낫다는 이야기인것 같다.
결국 위에 정리한 내용과 크게 다르지 않은 내용이다.

그런데 만약 IEnumerable형을 .ToList()로 전환한다면?
List로 바로 반환하는것이 효율적일까, IEnumerable형을 ToList()로 변환시키는 것이 효율적일까?

그래서 좀 더 명확한 차이점을 찾아보다보니
즉시 평가(Immediate Evaluation)지연 평가(Lazy Evaluation) 라는 것을 보게 되었다.

  • List.FindAll(): 이 함수는 호출될 때 메모리에서 즉시 모든 요소를 필터링하고 해당하는 요소만을 새로운 리스트로 반환합니다. 이것은 즉시 평가(Immediate Evaluation)라고도 합니다. 따라서 데이터 세트가 크더라도 필터링 작업은 한 번에 이루어지며, 그 결과 새로운 리스트가 생성됩니다.
  • LINQ의 Where 함수: 이 함수는 필터링 조건을 정의하기만 하고 데이터에 대한 실제 평가는 필요할 때에만 수행됩니다. 이것을 지연 평가(Lazy Evaluation)라고 합니다. 따라서 Where 함수를 호출하더라도 실제 필터링은 언제든지 쿼리를 사용하는 시점에 발생합니다. 이는 필요할 때마다 쿼리가 평가되므로 데이터 크기가 크고 조건이 복잡한 경우에 유용합니다. 그러나 쿼리를 반복적으로 호출할 때마다 매번 새로운 평가가 이루어지므로 성능 저하의 가능성이 있습니다.
    (출처 - 챗GPT)

1. 지연 평가(Lazy Evaluation)

쿼리를 정의한다는것은 단순 수행 과정을 정의한것에 지나지 않는다.
즉, 쿼리가 정의되더라도 즉시 결과물을 생성하는것이 아니라
직접 순회과정이 이루어져야 실제 결과물이 생성된다. (메모리에 할당되지 않는다)
👉 반환되는 속도가 빠르다! 위 게시물에서 CheckSum을 사용하지 않았을 때 Where-IEnumerable 조합의 속도가 아주 빠른 이유를 알 수 있다.

2. 즉시 평가(Immediate Evaluation)

일반 변수를 사용하는 것처럼 즉시 해당 값에 접근할수 있다. (메모리에 할당된다)
Array, List등이 이에 해당한다.

결론

Where().ToList() 의 경우에도 Where() 구문을 거쳐야 하므로 실제 메모리에 할당되지 않고,
반환값을 실제로 사용하려 할때 결과물이 생성 될것이다. (테스트는 필요함)

결국, 단순 열거만 필요한 경우에는 무조건 Where() 구문이 효율적이고 (훨씬 빠르고 메모리도 잡아먹지 않음),
해당 결과물을 가공하거나 추가 작업이 더 필요한 경우에는 FindAll()이나 Where().ToList()나.. 비슷할 것 같다.

  1. LINQ의 Where().ToList() 사용:
    LINQ의 Where() 함수는 지연 평가를 사용하므로 필요한 시점에만 필터링이 이루어집니다. 이는 결과적으로 LINQ를 사용하여 생성된 리스트인 list1이 필요한 시점에만 평가되어 생성됩니다. 이는 필요한 데이터만을 메모리에 로드하고, 필터링 작업이 실제로 필요한 시점에만 이루어지므로 효율적입니다. 그러나 필요한 시점에 평가되어 생성되므로 해당 시점까지 지연될 수 있습니다.
  2. List.FindAll() 사용:
    List.FindAll() 메서드는 필터링을 즉시 수행하고 결과를 반환합니다. 이는 모든 요소를 메모리에 로드하고 즉시 필터링을 수행하기 때문에 초기 생성 비용은 더 높을 수 있습니다. 그러나 결과는 즉시 사용 가능하므로 필요한 시점에 대한 지연이 없습니다.
    (출처 - 챗GPT)

추가글 (24-02-21)

성능 테스트 관련 좋은 글이 있어서 추가한다.
.ToList() 하는 경우에도 성능에 크게 영향이 없는것 같다.
Update 구문에서는 LINQ를 사용하지 않는걸로.

LINQ 성능 테스트
https://medium.com/swlh/is-using-linq-in-c-bad-for-performance-318a1e71a732
LINQ 가비지 생성
https://www.jacksondunstan.com/articles/4840



참고
👀 https://myoung-min.tistory.com/22
👀 https://mentum.tistory.com/548

0개의 댓글