결과를 캐시하지 않는 방식으로 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.ReadAllLines 와 File.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.