결과를 캐시하지 않는 방식으로 LINQ를 구현하면 어떤 이점이 있습니까?


20

이것은 LINQ를 사용하여 발을 적시는 사람들에게 알려진 함정입니다.

public class Program
{
    public static void Main()
    {
        IEnumerable<Record> originalCollection = GenerateRecords(new[] {"Jesse"});
        var newCollection = new List<Record>(originalCollection);

        Console.WriteLine(ContainTheSameSingleObject(originalCollection, newCollection));
    }

    private static IEnumerable<Record> GenerateRecords(string[] listOfNames)
    {
        return listOfNames.Select(x => new Record(Guid.NewGuid(), x));
    }

    private static bool ContainTheSameSingleObject(IEnumerable<Record>
            originalCollection, List<Record> newCollection)
    {
        return originalCollection.Count() == 1 && newCollection.Count() == 1 &&
                originalCollection.Single().Id == newCollection.Single().Id;
    }

    private class Record
    {
        public Guid Id { get; }
        public string SomeValue { get; }

        public Record(Guid id, string someValue)
        {
            Id = id;
            SomeValue = someValue;
        }
    }
}

원본 컬렉션을 만들기 위해 제공된 각 이름에 대해 select 함수가 계속 재평가되고 결과 Record개체가 새로 만들어 지기 때문에 "False"가 인쇄 됩니다. 이 문제를 해결하기 위해 ToList의 끝에 간단한 호출을 추가 할 수 있습니다 GenerateRecords.

이런 식으로 구현함으로써 Microsoft는 어떤 이점을 얻었습니까?

구현이 단순히 결과를 내부 배열로 캐시하지 않는 이유는 무엇입니까? 발생하는 일 중 하나의 특정 부분 은 실행 지연 일 수 있지만이 동작 없이는 여전히 구현 될 수 있습니다.

LINQ가 반환 한 컬렉션의 특정 멤버가 평가되면 내부 참조 / 복사를 유지하지 않고 대신 기본 결과로 동일한 결과를 다시 계산하면 어떤 이점이 있습니까?

반복해서 재 계산 된 동일한 컬렉션 멤버에 대한 논리에 특별한 요구가있는 경우, 선택적 매개 변수를 통해 지정할 수 있고 기본 동작은 그렇지 않은 것으로 보입니다. 또한 지연된 실행으로 얻을 수있는 속도 이점은 궁극적으로 동일한 결과를 지속적으로 다시 계산하는 데 걸리는 시간에 비해 줄어 듭니다. 마지막으로 이것은 LINQ를 처음 사용하는 사람들에게는 혼란스러운 블록이며 궁극적으로 모든 사람의 프로그램에서 미묘한 버그로 이어질 수 있습니다.

이 점에서 어떤 이점이 있으며, Microsoft가이 계획을 매우 신중하게 결정한 이유는 무엇입니까?


1
GenerateRecords () 메서드에서 ToList ()를 호출하면됩니다. return listOfNames.Select(x => new Record(Guid.NewGuid(), x)).ToList(); "캐시 된 사본"을 제공합니다. 문제 해결됨.
Robert Harvey

1
나는 알고 있지만, 왜 그들이 처음에 이것을 필요로했는지 궁금했습니다.
Panzercrisis

11
게으른 평가에는 "오래, 마지막으로 요청한 이후로이 레코드가 변경되었습니다. 여기에 새 버전이 있습니다."라는 코드 이점이 있습니다. 이는 코드 예제와 정확히 일치합니다.
Robert Harvey

지난 6 개월 동안 여기서 거의 똑같은 문구를 읽었지만 지금은 찾지 못했습니다. 내가 찾을 수있는 가장 가까운 것은 stackoverflow에서 2016 년이었습니다 : stackoverflow.com/q/37437893/391656
Mr.Mindor

29
만료 정책이없는 캐시 이름 인 "memory leak"이 있습니다. 무효화 정책이없는 캐시 이름 인 "bug farm"이 있습니다. 가능한 모든 LINQ 쿼리에 대해 항상 올바른 만료 및 무효화 정책을 제안하지 않으면 질문 자체가 답변됩니다.
Eric Lippert

답변:


51

결과를 캐시하지 않는 방식으로 LINQ를 구현하면 어떤 이점이 있습니까?

결과를 캐싱하는 것은 모든 사람에게 효과적이지 않을 것입니다. 소량의 데이터가있는 한 좋습니다. 잘 됐네요. 그러나 데이터가 RAM보다 큰 경우 어떻게해야합니까?

LINQ와는 관련이 없지만 IEnumerable<T>일반적으로 인터페이스 와 관련이 있습니다.

File.ReadAllLinesFile.ReadLines 의 차이점 입니다. 하나는 RAM에 전체 파일을 읽을 것입니다, 당신은 라인으로 라인에 큰 파일로 작업 할 수 있도록 (그들은만큼 다른 하나는, 그것을 줄 것이다 줄 바꿈).

당신은 쉽게 캐시 모든 것을 당신은 어느 쪽인지를 호출 시퀀스 구체화하여 캐시 할 수 있습니다 .ToList()또는 .ToArray()그것에을. 그러나 캐시 하지 않으려 는 사람들은 그렇게 할 기회가 없습니다 .

관련 메모에서 다음을 어떻게 캐시합니까?

IEnumerable<int> AllTheZeroes()
{
    while(true) yield return 0;
}

당신은 할 수 없습니다. 그것이 IEnumerable<T>존재하는 이유 입니다.


2
마지막 예제는 끝없는 0의 문자열이 아닌 실제 무한 시리즈 (예 : Fibonnaci)라면 더 매력적입니다.
Robert Harvey

23
@RobertHarvey 사실, 나는 이해할 논리가 전혀 없을 때 끝없는 0의 스트림임을 발견하는 것이 더 쉽다고 생각했습니다.
nvoigt

2
int i=1; while(true) { i++; yield fib(i); }
Robert Harvey

2
내가 생각한 예는 Enumerable.Range(1,int.MaxValue)사용할 메모리 양에 대한 하한을 쉽게 계산할 수 있다는 것입니다.
Chris

4
내가 선을 따라 본 또 다른 것은 무한 난수 스트림을 생성하는 while (true) return ...것이 었 while (true) return _random.Next();습니다.
Chris

24

이런 식으로 구현함으로써 Microsoft는 어떤 이점을 얻었습니까?

단정? 내 말은, 열거 가능한 핵심은 호출 사이에서 바뀔 수 있다는 것을 의미합니다. 캐싱하면 잘못된 결과가 발생하고 "캐시를 무효화하는시기 / 어떻게?"전체 웜이 열립니다.

LINQ가 원래 LINQ를 데이터 소스 (엔터티 프레임 워크 또는 SQL과 같은)에 수행하는 수단으로 설계 되었다고 생각하면 데이터베이스 가 수행 하는 것이므로 열거 형 변경 될 것 입니다.

또한 단일 책임 원칙 문제가 있습니다. 쿼리하고 캐시하는 코드를 작성하고 캐싱을 제거하는 것보다 작동하고 캐싱을 구축하는 쿼리 코드를 만드는 것이 훨씬 쉽습니다.


3
그것이 ICollection존재하고 아마도 OP가 기대 IEnumerable하는 방식으로 행동 할 것입니다.
Caleth

IEnumerable <T>을 사용하여 열린 데이터베이스 커서를 읽는 경우 ACID 트랜잭션이있는 데이터베이스를 사용하는 경우 결과가 변경되지 않아야합니다.
Doug

4

LINQ는 기능 프로그래밍 언어에서 널리 사용되는 Monad 패턴일반적인 구현이며 처음부터 의도 되었기 때문에 Monad는 항상 동일한 호출 순서에서 실제로 동일한 값을 산출하도록 제한되지 않습니다 함수형 프로그래밍에서이 속성으로 인해 인기가 높아 순수 함수의 결정 론적 동작을 피할 수 있습니다).


4

언급되지 않은 또 다른 이유는 가비지 중간 결과를 만들지 않고 다른 필터와 변환을 연결할 수 있기 때문입니다.

예를 들면 다음과 같습니다.

cars.Where(c => c.Year > 2010)
.Select(c => new { c.Model, c.Year, c.Color })
.GroupBy(c => c.Year);

LINQ 메소드가 결과를 즉시 계산하면 3 가지 컬렉션이 있습니다.

  • 결과
  • 결과 선택
  • GroupBy 결과

그 중 우리는 마지막 것에 만 관심이 있습니다. 중간 결과를 액세스 할 수 없기 때문에 중간 결과를 저장할 필요가 없으며 이미 필터링하여 연도별로 그룹화 한 자동차에 대해서만 알고 싶습니다.

이러한 결과를 저장해야하는 경우 해결책은 간단합니다. 즉, 호출을 분리하고 호출 .ToList()하여 변수에 저장하십시오.


참고로 JavaScript에서 Array 메서드는 실제로 결과를 즉시 반환하므로주의하지 않으면 더 많은 메모리를 소비 할 수 있습니다.


3

기본적으로,이 코드 - 퍼팅 Guid.NewGuid ()돌며 Select문 - 매우 의심스러운입니다. 이것은 분명히 어떤 종류의 코드 냄새입니다!

이론적으로, 우리는 Select성명서가 반드시 새로운 데이터를 생성 할 것이 아니라 기존 데이터를 검색 할 것을 기대할 것 입니다. Select가 여러 소스의 데이터를 결합하여 다른 모양의 결합 된 컨텐츠를 생성하거나 추가 열을 계산하는 것이 합리적이지만 여전히 기능적이고 순수 할 것으로 기대할 수 있습니다. NewGuid ()내부를 넣으면 작동하지 않고 순수하지 않습니다.

데이터 생성은 선택과 분리되어 일종의 생성 작업을 수행 할 수 있으므로 선택은 순수하고 재사용 가능한 상태로 유지되거나 선택은 한 번만 수행되고 랩핑 / 보호됩니다. 는 IS .ToList ()제안.

그러나 분명히 문제는 캐싱 부족보다는 선택 내부의 창조물 혼합을 보여줍니다. NewGuid()선택 내부에 넣는 것은 프로그래밍 모델의 부적절한 혼합으로 보입니다.


0

지연된 실행을 통해 LINQ 코드 작성 (정확하고 IEnumerable<T>)을 사용 하여 결과를 즉시 계산하여 메모리에 저장할지 여부를 명시 적으로 선택할 수 있습니다. 다시 말해, 프로그래머는 애플리케이션에 가장 적합한 계산 시간과 저장 공간의 균형을 선택할 수 있습니다.

대부분의 응용 프로그램은 결과를 즉시 원하므로 LINQ의 기본 동작이어야합니다. 그러나 List<T>.ConvertAllLINQ가 도입 될 때까지 실행을 연기 할 수있는 방법이 없었지만,이 동작을 제공하고 프레임 워크가 생성 된 이후에 수행 된 다른 많은 API (예 :)가 있습니다. 다른 답변에서 알 수 있듯이 즉시 실행을 사용할 때 (사용 가능한 모든 스토리지를 소진하여) 불가능한 특정 유형의 계산을 가능하게하는 전제 조건입니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.