Microsoft MVP성태의 닷넷 이야기
.NET Framework: 247. LINQ에서의 Max 기능 구현 [링크 복사], [링크+제목 복사],
조회: 33023
글쓴 사람
정성태 (techsharer at outlook.com)
홈페이지
첨부 파일

LINQ에서의 Max 기능 구현


이 글은, 다음의 질문 때문에 씌여진 것입니다.

LINQ 가장 큰 값 하나만 가져오기.
; http://www.devpia.com/MAEUL/Contents/Detail.aspx?BoardID=17&MAEULNO=8&no=140468&ref=140468&page=1

자... 문제를 여기다 다시 써볼까요?

Program tt = new Program();

tt.data = new List<FileInfo>();
tt.data.Add(new FileInfo { id = 1, filename = "AUpIs80.log", sended = true });
tt.data.Add(new FileInfo { id = 2, filename = "adminxplore.exe", sended = false });
tt.data.Add(new FileInfo { id = 3, filename = "LMDebug.log", sended = false });
tt.data.Add(new FileInfo { id = 4, filename = "LMDebug1.log", sended = false });
tt.data.Add(new FileInfo { id = 5, filename = "LMDebug1.log", sended = false });

var linq = from a in tt.data
    orderby a.id
    where a.sended == false
    select a.id;

안녕하세요 저런 코드에서 id가 가장 크거나 작은 것 하나만 셀렉트 하는 방법이 없나요? Max를 이용 하면 될거 같은데 쉽지 않네요...
그리고 저렇게 var linq에 담게 되면 실제 메모리에 새로 만들어 지는건가요? 아니면 LIST data의 주소 값만 참조 하는건가요?

우선, 원하는 값이 무엇이냐에 따라 달라집니다. 간단하게는 가장 큰 "id" 값을 원할 수 있을 텐데요. 그런 경우에는 굳이 LINQ까지 갈 필요없이 Max 확장 함수를 이용해서 다음과 같이 구할 수 있습니다.

var max = tt.data.Where(a => a.sended == false).Max(a => a.id);

결과: max == 5

그런데, max == 5인 FileInfo 개체를 원한다면 어떻게 해야 할까요?

우선, 마이크로소프트의 예제 코드에 소개된 방식을 이용하여,

How to: Find the Maximum Value in a Numeric Sequence (LINQ to SQL)
; https://docs.microsoft.com/en-us/dotnet/framework/data/adonet/sql/linq/find-the-maximum-value-in-a-numeric-sequence

다음과 같이 구현되어질 수 있습니다.

var linq = from a in tt.data
            where a.sended == false && a.id == tt.data.Max(b => b.id)
            select a;

Console.WriteLine(linq.First().id);

결과야 나오긴 했지만, LINQ 구문을 잠시 살펴보면 매우 비효율적이라는 것을 알 수 있습니다. 왜냐하면, tt.data.Max(...)는 data에 있는 모든 요소들을 열람하게 되는데 그것의 실행 횟수가 역시 data 요소 수만큼 발생하기 때문입니다.

따라서, 이러한 쓸데없는 오버헤드를 줄이기 위해서는 다음과 같이 바꿔주는 것이 좋습니다.

var max = tt.data.Where(a => a.sended == false).Max(a => a.id);

var linq2 = from a in tt.data
            where a.sended == false && a.id == max
            select a;

그런데... 왠지 너무 길다는 생각이 안드세요? 차라리 첫 번째 "var max = ...Max(...)" 구문에서 객체값을 반환해 준다면 좋겠다는 생각이 듭니다. 아쉽게도 기본 제공되는 Max 확장 함수에서는 그에 전달된 람다식의 값을 반환하게 되어 있기 때문에 약간의 변형을 해주어야 합니다.

이를 위해서 Max에 대한 별도의 확장 함수를 제공해 주면 되는데, 이에 대해서는 다음의 글에서 자세하게 설명해 주고 있습니다.

Custom C# 3.0 LINQ Max Extension Method
; http://geekswithblogs.net/michelotti/archive/2009/02/06/custom-c-3.0-linq-max-extension-method.aspx

위의 글에 따라 Max 함수를 만들어 주면,

public static class ExtensionMethod
{
    public static FileInfo Max<TCompare>(this IEnumerable<FileInfo> collection, Func<FileInfo, TCompare> func) where TCompare : IComparable<TCompare>
    {
        FileInfo maxItem = null;
        TCompare maxValue = default(TCompare);

        foreach (var item in collection)
        {
            TCompare temp = func(item);
            if (maxItem == null || temp.CompareTo(maxValue) > 0)
            {
                maxValue = temp;
                maxItem = item;
            }
        }

        return maxItem;
    }
}

이제 다음과 같이 자연스럽게 Max 값을 구해낼 수 있습니다.

var max = tt.data.Where(a => a.sended == false).Max(a => a.id);
Console.WriteLine(max.id);




그런데, 한번 더 생각해 볼까요?

확장 함수 없이, 애당초 다음과 같은 구문으로 max를 구했으면 더 편했을 거 아닌가요?

var max = tt.data.Where(a => a.sended == false).Max(a => a.id);

==> var max = tt.data.Where(a => a.sended == false).Max(a => a);

실제로 위의 구문을 실행하면 다음과 같은 오류가 발생합니다.

Unhandled Exception: System.ArgumentException: At least one object must implement IComparable.
   at System.Collections.Comparer.Compare(Object a, Object b)
   at System.Collections.Generic.ObjectComparer`1.Compare(T x, T y)
   at System.Linq.Enumerable.Max[TSource](IEnumerable`1 source)
   at System.Linq.Enumerable.Max[TSource,TResult](IEnumerable`1 source, Func`2 selector)
   at ConsoleApplication1.Program.Main(String[] args) in D:\...[생략]...\Program.cs:line 36

아하... 그저 FileInfo 개체에 비교가 가능하도록 만들어주면 되겠군요.

public class FileInfo : IComparable
{
    public int id { get; set; }
    public string filename { get; set; }
    public bool sended { get; set; }

    public int CompareTo(object other)
    {
        FileInfo second = other as FileInfo;
        if (second == null)
        {
            return 1;
        }

        if (second.id > id) return -1;
        if (second.id == id) return 0;
        return 1;
    }
}

var max = tt.data.Where(a => a.sended == false).Max(a => a); // 정상적으로 실행됨.
Console.WriteLine(max.id);




이 정도면, 웬만큼 Max 관련해서는 의문점이 풀렸겠지요. ^^ 그럼, 부가적으로 마지막 질문으로 넘어가 볼까요?

그리고 저렇게 var linq에 담게 되면 실제 메모리에 새로 만들어 지는건가요? 아니면 LIST data의 주소 값만 참조 하는건가요?

이에 대해서는 간단하게 테스트 해서 알아보는 방법이 있습니다.

새롭게 메모리가 할당되는 것이라면, 기존 개체의 값을 바꿨다고 해서 LINQ 계산값이 바뀌지는 않았을 것이라는 간단한 원칙을 이용하면 되는 것이지요. ^^

var max4 = tt.data.Where(a => a.sended == false).Max(a => a);

tt.data[4].filename = "test";

Console.WriteLine(max4.filename);

출력: test

결과를 보니, tt.data 목록 안에 있는 개체의 참조값임을 알 수 있습니다. 메모리 할당이 아닌 참조값만 넘어왔다는 것입니다.

첨부된 파일은 위의 코드를 포함한 예제 프로젝트입니다.




[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]







[최초 등록일: ]
[최종 수정일: 6/26/2021]

Creative Commons License
이 저작물은 크리에이티브 커먼즈 코리아 저작자표시-비영리-변경금지 2.0 대한민국 라이센스에 따라 이용하실 수 있습니다.
by SeongTae Jeong, mailto:techsharer at outlook.com

비밀번호

댓글 작성자
 



2011-10-13 10시50분
[강덕훈] 와우~ 감사합니다. 한시간 넘게 곰곰히 생각 할 수 있는 답변이었습니다.

IComparable 에 대해서 확실히 배웠습니다.

컬럭센 관련 클래스에 Max 함수를 호출해도 컴파일은 되는데 런타임에서 에러가 나는게 신기하네요.

템플릿 클래스(FileInfo) 가 IComparable 를 임플리먼트 하지 않았다는걸 컴파일 단계에서 알려주면 더 좋을텐데 말이죠...

아마 제너릭 개념이 추가되어서 컴파일러가 어려움이 있는 듯 한데...

정말 많이 배우고 갑니다. 아 그리고

이 글의 맨 처음 부분에
var max = tt.data.Where(a => a.sended == false).Max(a => a.id);

결과: max == 5

이 부분도 런타임 오류가 나네요 ^^
[guest]
2011-10-13 01시18분
max에서 IComparable이 없다고 컴파일 단계에서 알려주지 않는 것은, Max 확장 함수에서 객체가 아닌 함수를 받기 때문입니다. 함수 자체는 IComparable 구현 여부를 알 수 없지요. ^^

그리고, 런타임 오류가 난다는 코드는 제가 이 글에 실린 예제에서 발생하는 것인가요? 이상한데요. 제가 첨부한 소스에서는 런타임 오류가 발생하지 않습니다. 혹시, 현재 런타임 오류가 발생하는 간략화된 소스를 올려주시겠어요? ^^
정성태
2011-10-13 02시36분
[강덕훈] 아~ 그렇군요.. 잘 배우고 갑니다. 아직 부족한게 너무 많네요 ㅋ


아뇨 예제는 IComparable 를 구현 했으니 오류가 안납니다.
이 글의 순서상 처음엔 IComparable를 구현 안했으니 "결과: max == 5" 안나온다는 거죠 ^^
[guest]
2011-10-13 02시45분
음... 이상한데요. ^^
분명히, IComparable 구현 여부와 상관없이 아래의 구문은 예외 없이 잘 실행됩니다.

var max = tt.data.Where(a => a.sended == false).Max(a => a.id);
결과: max == 5
정성태
2011-10-13 03시08분
max에서 IComparable에 대한 컴파일 단계의 오류에 대해서 좀 더 생각해 보았는데... 제가 잘못 대답을 했군요. ^^ 어차피 함수를 받긴 하지만, generic에서 받은 T에 대해 where 구문을 주면 되기 때문에 가능하군요.

가령 다음과 같은 코드입니다. (아래의 코드를 담아서, 첨부된 예제 프로젝트의 코드를 업데이트 했습니다.)

        public static TSource Max4<TSource>(this IEnumerable<TSource> collection, Func<TSource, TSource> func)
            where TSource : IComparable<TSource>
        {
            Console.WriteLine("Max4(TSource, TCompare) called!");

            TSource maxItem = default(TSource);
            TSource maxValue = default(TSource);

            foreach (var item in collection)
            {
                TSource temp = func(item);
                if (maxItem == null || temp.CompareTo(maxValue) > 0)
                {
                    maxValue = temp;
                    maxItem = item;
                }
            }

            return maxItem;
        }
정성태
2011-10-13 05시42분
[강덕훈] 아~ ㅋㅋ 감사합니다.

c#에 입문 한지 얼마 안되서 궁금한게 너무 많네요 ㅋ 하루에도 몇가지씩 생기네요

자주 방문 하겠습니다. ㅋ
[guest]
2012-03-06 09시47분
[김성일] 좋은글 감사합니다.~~~
[guest]
2023-04-26 09시54분
[밤밤] 내용 감사합니다
[guest]
2023-04-26 11시56분
@밤밤 귀찮게 고친 티가 나는군요. ^^; (퇴근 후에 수정하면 이후부터는 덧글이 안 될 것입니다.)
정성태
2023-04-26 08시18분
.NET 6 BCL부터 본문에 해당하는 목적의 LINQ 확장 메서드(MaxBy, MinBy)가 추가됐습니다.

Bite-Size .NET 6 - MaxBy() and MinBy() in LINQ
; https://exceptionnotfound.net/bite-size-dotnet-6-maxby-and-minby-in-linq/
정성태

1  2  3  4  5  6  7  8  [9]  10  11  12  13  14  15  ...
NoWriterDateCnt.TitleFile(s)
13718정성태8/27/20247455오류 유형: 921. Visual C++ - error C1083: Cannot open include file: 'float.h': No such file or directory [2]
13717정성태8/26/20247057VS.NET IDE: 192. Visual Studio 2022 - Windows XP / 2003용 C/C++ 프로젝트 빌드
13716정성태8/21/20246770C/C++: 167. Visual C++ - 윈도우 환경에서 _execv 동작 [1]
13715정성태8/19/20247404Linux: 78. 리눅스 C/C++ - 특정 버전의 glibc 빌드 (docker-glibc-builder)
13714정성태8/19/20246773닷넷: 2295. C# 12 - 기본 생성자(Primary constructors) (책 오타 수정) [3]
13713정성태8/16/20247516개발 환경 구성: 721. WSL 2에서의 Hyper-V Socket 연동
13712정성태8/14/20247231개발 환경 구성: 720. Synology NAS - docker 원격 제어를 위한 TCP 바인딩 추가
13711정성태8/13/20248088Linux: 77. C# / Linux - zombie process (defunct process) [1]파일 다운로드1
13710정성태8/8/20248018닷넷: 2294. C# 13 - (6) iterator 또는 비동기 메서드에서 ref와 unsafe 사용을 부분적으로 허용파일 다운로드1
13709정성태8/7/20247770닷넷: 2293. C# - safe/unsafe 문맥에 대한 C# 13의 (하위 호환을 깨는) 변화파일 다운로드1
13708정성태8/7/20247567개발 환경 구성: 719. ffmpeg / YoutubeExplode - mp4 동영상 파일로부터 Audio 파일 추출
13707정성태8/6/20247797닷넷: 2292. C# - 자식 프로세스의 출력이 4,096보다 많은 경우 Process.WaitForExit 호출 시 hang 현상파일 다운로드1
13706정성태8/5/20247923개발 환경 구성: 718. Hyper-V - 리눅스 VM에 새로운 디스크 추가
13705정성태8/4/20248175닷넷: 2291. C# 13 - (5) params 인자 타입으로 컬렉션 허용 [2]파일 다운로드1
13704정성태8/2/20248137닷넷: 2290. C# - 간이 dotnet-dump 프로그램 만들기파일 다운로드1
13703정성태8/1/20247466닷넷: 2289. "dotnet-dump ps" 명령어가 닷넷 프로세스를 찾는 방법
13702정성태7/31/20247886닷넷: 2288. Collection 식을 지원하는 사용자 정의 타입을 CollectionBuilder 특성으로 성능 보완파일 다운로드1
13701정성태7/30/20248146닷넷: 2287. C# 13 - (4) Indexer를 이용한 개체 초기화 구문에서 System.Index 연산자 허용파일 다운로드1
13700정성태7/29/20247771디버깅 기술: 200. DLL Export/Import의 Hint 의미
13699정성태7/27/20248273닷넷: 2286. C# 13 - (3) Monitor를 대체할 Lock 타입파일 다운로드1
13698정성태7/27/20248225닷넷: 2285. C# - async 메서드에서의 System.Threading.Lock 잠금 처리파일 다운로드1
13697정성태7/26/20247934닷넷: 2284. C# - async 메서드에서의 lock/Monitor.Enter/Exit 잠금 처리파일 다운로드1
13696정성태7/26/20247471오류 유형: 920. dotnet publish - error NETSDK1047: Assets file '...\obj\project.assets.json' doesn't have a target for '...'
13695정성태7/25/20247462닷넷: 2283. C# - Lock / Wait 상태에서도 STA COM 메서드 호출 처리파일 다운로드1
13694정성태7/25/20247923닷넷: 2282. C# - ASP.NET Core Web App의 Request 용량 상한값 (Kestrel, IIS)
13693정성태7/24/20247249개발 환경 구성: 717. Visual Studio - C# 프로젝트에서 레지스트리에 등록하지 않은 COM 개체 참조 및 사용 방법파일 다운로드1
1  2  3  4  5  6  7  8  [9]  10  11  12  13  14  15  ...