소개
이 자습서에서는 .NET 및 C# 언어의 기능을 설명합니다. 당신은 다음을 배우게 됩니다:
- LINQ를 사용하여 시퀀스를 생성합니다.
- LINQ 쿼리에서 쉽게 사용할 수 있는 메서드를 작성합니다.
- 즉시 평가와 지연 평가를 구분합니다.
마술사인 파로 셔플의 기본 기술 중 하나를 보여 주는 애플리케이션을 빌드하여 이러한 기술을 배웁니다. 파로 셔플은 카드 더미를 정확히 절반으로 나눈 다음, 양쪽에서 번갈아가며 카드를 섞어 원래의 카드 더미를 재구성하는 기술입니다.
마술사는 모든 카드가 각 순서 섞기 후 알려진 위치에 있고 순서가 반복 패턴이기 때문에 이 기술을 사용합니다.
이 자습서에서는 데이터 시퀀스를 조작하는 방법에 대해 가볍게 살펴봅니다. 애플리케이션은 카드 덱을 생성하고, 여러 차례 섞기 작업을 수행하며, 매번 그 결과를 기록합니다. 또한 업데이트된 순서와 원래 순서를 비교합니다.
이 자습서에는 여러 단계가 있습니다. 각 단계 후에 애플리케이션을 실행하고 진행률을 확인할 수 있습니다. dotnet/samples GitHub 리포지토리에서 완성된 샘플을 볼 수도 있습니다. 다운로드 지침은 샘플 및 자습서참조하세요.
필수 조건
- 최신 .NET SDK
- Visual Studio Code 편집기
- C# 개발 키트
애플리케이션 만들기
새 애플리케이션을 만듭니다. 명령 프롬프트를 열고 애플리케이션에 대한 새 디렉터리를 만듭니다. 현재 디렉터리로 설정하십시오. 명령 프롬프트에 명령을 dotnet new console -o LinqFaroShuffle 입력합니다. 이 명령은 기본 "Hello World" 애플리케이션에 대한 시작 파일을 만듭니다.
이전에 C#을 사용한 적이 없는 경우 이 자습서 에서는 C# 프로그램의 구조를 설명합니다. LINQ에 대해 자세히 알아보려면 이 내용을 읽은 다음 여기로 돌아갈 수 있습니다.
데이터 집합 만들기
팁 (조언)
이 자습서에서는 샘플 코드와 일치하도록 LinqFaroShuffle라는 네임스페이스에 코드를 구성하거나 전역 네임스페이스를 사용할 수 있습니다. 네임스페이스를 사용하도록 선택한 경우 모든 클래스와 메서드가 동일한 네임스페이스 내에 일관되게 있는지 확인하거나 필요에 따라 적절한 using 문을 추가합니다.
카드 덱이란 무엇으로 구성되는지 생각해보세요. 카드 놀이의 갑판에는 4 개의 정장이 있으며 각 정장에는 13 개의 값이 있습니다. 일반적으로 Card 클래스를 즉시 만들고 Card 개체 컬렉션을 수동으로 채우는 것을 고려할 수 있습니다. LINQ를 사용하면 카드 덱을 만드는 일반적인 방법보다 더 간결할 수 있습니다. 클래스를 Card 만드는 대신, 정장과 순위를 나타내는 두 개의 시퀀스를 만듭니다. 문자열로 순위와 적합을 생성하는 IEnumerable<T> 쌍을 만듭니다.
static IEnumerable<string> Suits()
{
yield return "clubs";
yield return "diamonds";
yield return "hearts";
yield return "spades";
}
static IEnumerable<string> Ranks()
{
yield return "two";
yield return "three";
yield return "four";
yield return "five";
yield return "six";
yield return "seven";
yield return "eight";
yield return "nine";
yield return "ten";
yield return "jack";
yield return "queen";
yield return "king";
yield return "ace";
}
이러한 메서드를 Program.cs 파일의 Console.WriteLine 문 아래에 배치합니다, . 이 두 메서드는 실행 시 yield return 구문을 사용하여 시퀀스를 생성합니다. 컴파일러는 IEnumerable<T>을(를) 구현하는 개체를 생성하고, 문자열 시퀀스를 요청에 따라 생성합니다.
이제 이러한 반복기 메서드를 사용하여 카드 데크를 만듭니다. LINQ 쿼리를 파일 맨 위에 배치합니다 Program.cs . 모양은 다음과 같습니다.
var startingDeck = from s in Suits()
from r in Ranks()
select (Suit: s, Rank: r);
// Display each card that's generated and placed in startingDeck
foreach (var card in startingDeck)
{
Console.WriteLine(card);
}
여러 from 절은 첫 번째 시퀀스의 각 요소를 두 번째 시퀀스의 각 요소와 결합하여 단일 시퀀스를 생성하는 SelectMany을(를) 만듭니다. 이 예제에서는 순서가 중요합니다. 첫 번째 소스 시퀀스의 첫 번째 요소(Suits)는 두 번째 시퀀스(Ranks)의 모든 요소와 결합됩니다. 이 프로세스는 첫 번째 소송의 모든 13 카드를 생성합니다. 해당 프로세스는 첫 번째 시퀀스(Suits)의 각 요소와 함께 반복됩니다. 최종 결과는 카드가 수트를 기준으로 먼저 정렬되고, 그 다음에 값에 따라 정렬됩니다.
앞의 샘플에서 사용된 쿼리 구문에서 LINQ를 작성하든 메서드 구문을 대신 사용하든 항상 한 형식의 구문에서 다른 구문으로 이동하는 것이 가능합니다. 쿼리 구문으로 작성된 이전 쿼리는 다음과 같이 메서드 구문으로 작성할 수 있습니다.
var startingDeck = Suits().SelectMany(suit => Ranks().Select(rank => (Suit: suit, Rank: rank )));
컴파일러는 쿼리 구문으로 작성된 LINQ 문을 동등한 메서드 호출 구문으로 변환합니다. 따라서 구문 선택에 관계없이 두 버전의 쿼리는 동일한 결과를 생성합니다. 상황에 가장 적합한 구문을 선택합니다. 예를 들어 일부 멤버가 메서드 구문에 어려움을 겪고 있는 팀에서 작업하는 경우 쿼리 구문을 사용하는 것을 선호합니다.
이 시점에서 빌드한 샘플을 실행합니다. 데크에 52장의 카드가 모두 표시됩니다. 디버거에서 이 샘플을 실행하여 Suits() 및 Ranks() 메서드가 실행되는 방식을 관찰하는 것이 유용할 수 있습니다. 각 시퀀스의 각 문자열이 필요에 따라 생성된다는 것을 분명히 알 수 있습니다.
순서 조작
다음으로, 데크에서 카드를 섞는 방법에 집중합니다. 좋은 셔플의 첫 번째 단계는 덱을 두 개로 나누는 것입니다.
Take LINQ API의 일부인 메서드 및 Skip 메서드는 해당 기능을 제공합니다.
foreach 루프 다음에 그것들을 배치합니다.
var top = startingDeck.Take(26);
var bottom = startingDeck.Skip(26);
그러나 표준 라이브러리에서 활용할 순서 섞기 메서드는 없으므로 직접 작성해야 합니다. 만드는 순서 섞기 메서드는 LINQ 기반 프로그램에서 사용하는 몇 가지 기술을 보여 주므로 이 프로세스의 각 부분은 단계별로 설명됩니다.
LINQ 쿼리의 결과와 IEnumerable<T> 상호 작용하는 방법에 기능을 추가하려면 확장 메서드라는 특수한 종류의 메서드를 작성합니다. 확장 메서드는 기능을 추가하려는 원래 형식을 수정하지 않고도 기존 형식에 새 기능을 추가하는 특수한 용도의 정적 메서드 입니다.
호출된 프로그램에 새 Extensions.cs 클래스 파일을 추가하여 확장 메서드에 새 홈을 제공한 다음, 첫 번째 확장 메서드 빌드를 시작합니다.
public static class CardExtensions
{
extension<T>(IEnumerable<T> sequence)
{
public IEnumerable<T> InterleaveSequenceWith(IEnumerable<T> second)
{
// Your implementation goes here
return default;
}
}
}
비고
Visual Studio 이외의 편집기(예: Visual Studio Code)를 사용하는 경우 확장 메서드에 액세스할 수 있도록 using LinqFaroShuffle; 파일의 맨 위에 추가 해야 할 수 있습니다. Visual Studio는 이 using 문을 자동으로 추가하지만 다른 편집기에서는 추가하지 않을 수 있습니다.
컨테이너는 extension 확장되는 형식을 지정합니다. 노드는 extension 컨테이너 내 의 모든 멤버에 대한 extension의 형식과 이름을 선언합니다. 이 예제에서는 확장하고 IEnumerable<T>매개 변수의 이름을 지정 sequence합니다.
확장 멤버 선언은 수신기 형식의 멤버인 것처럼 표시됩니다.
public IEnumerable<T> InterleaveSequenceWith(IEnumerable<T> second)
확장 형식의 멤버 메서드인 것처럼 메서드를 호출합니다. 또한 이 메서드 선언은 입력 및 출력 형식 IEnumerable<T>이 있는 표준 관용구를 따릅니다. 이러한 방법을 사용하면 LINQ 메서드를 함께 연결하여 더 복잡한 쿼리를 수행할 수 있습니다.
데크를 반으로 나누었으니, 이제 그 절반들을 함께 연결해야 합니다. 코드에서 이는 Take과 Skip에서 획득한 두 시퀀스를 동시에 열거하고, 요소를 인터리브하여 하나의 시퀀스를 만듦으로써 현재 순서가 섞인 카드 덱을 만드는 것을 의미합니다. 두 시퀀스로 작동하는 LINQ 메서드를 작성하려면 작동 방식을 IEnumerable<T> 이해해야 합니다.
IEnumerable<T> 인터페이스에는 다음 한 가지 메서드가 있습니다GetEnumerator. 반환된 GetEnumerator 개체에는 다음 요소로 이동하는 메서드와 시퀀스의 현재 요소를 검색하는 속성이 있습니다. 이러한 두 멤버를 사용하여 컬렉션을 열거하고 요소를 반환합니다. 이 Interleave 메서드는 반복기 메서드이므로 컬렉션을 빌드하고 컬렉션을 반환하는 대신 이전 코드에 표시된 구문을 사용합니다 yield return .
해당 메서드의 구현은 다음과 같습니다.
public IEnumerable<T> InterleaveSequenceWith(IEnumerable<T> second)
{
var firstIter = sequence.GetEnumerator();
var secondIter = second.GetEnumerator();
while (firstIter.MoveNext() && secondIter.MoveNext())
{
yield return firstIter.Current;
yield return secondIter.Current;
}
}
이제 이 메서드를 작성했으므로 Main 메서드로 돌아가서 카드 덱을 한 번 섞으세요.
var shuffledDeck = top.InterleaveSequenceWith(bottom);
foreach (var c in shuffledDeck)
{
Console.WriteLine(c);
}
비교
덱이 원래 상태로 돌아가는 데 필요한 셔플 횟수를 결정합니다. 알아보려면 두 시퀀스가 같은지 여부를 결정하는 메서드를 작성합니다. 해당 메서드가 있으면 데크를 순서대로 섞는 코드를 루프에 배치하고 데크가 다시 순서대로 돌아가는 시기를 확인합니다.
두 시퀀스가 같은지 확인하는 메서드를 작성하는 것은 간단해야 합니다. 그것은 당신이 카드를 섞으려고 쓴 방법과 유사한 구조입니다. 그러나 이번에는 각 요소에 대해 사용하는 yield return 대신 각 시퀀스의 일치하는 요소를 비교합니다. 전체 시퀀스가 열거되면 모든 요소가 일치하면 시퀀스는 동일합니다.
public bool SequenceEquals(IEnumerable<T> second)
{
var firstIter = sequence.GetEnumerator();
var secondIter = second.GetEnumerator();
while ((firstIter?.MoveNext() == true) && secondIter.MoveNext())
{
if ((firstIter.Current is not null) && !firstIter.Current.Equals(secondIter.Current))
{
return false;
}
}
return true;
}
이 메서드는 두 번째 LINQ 관용구인 터미널 메서드를 보여 줍니다. 시퀀스를 입력(또는 이 경우 두 시퀀스)으로 사용하고 단일 스칼라 값을 반환합니다. 터미널 메서드를 사용하는 경우 항상 LINQ 쿼리에 대한 메서드 체인의 최종 메서드입니다.
덱이 원래 순서로 돌아가는 시점을 결정할 때 사용하는 방법을 통해 이를 확인할 수 있습니다. 순서 섞기 코드를 루프 내부에 배치하고 메서드를 적용하여 SequenceEquals() 시퀀스가 원래 순서로 되돌아가면 중지합니다. 시퀀스 대신 단일 값을 반환하므로 모든 쿼리에서 항상 최종 메서드가 되는 것을 볼 수 있습니다.
var startingDeck = from s in Suits()
from r in Ranks()
select (Suit: s, Rank: r);
// Display each card generated and placed in startingDeck in the console
foreach (var card in startingDeck)
{
Console.WriteLine(card);
}
var top = startingDeck.Take(26);
var bottom = startingDeck.Skip(26);
var shuffledDeck = top.InterleaveSequenceWith(bottom);
var times = 0;
// Re-use the shuffle variable from earlier, or you can make a new one
shuffledDeck = startingDeck;
do
{
shuffledDeck = shuffledDeck.Take(26).InterleaveSequenceWith(shuffledDeck.Skip(26));
foreach (var card in shuffledDeck)
{
Console.WriteLine(card);
}
Console.WriteLine();
times++;
} while (!startingDeck.SequenceEquals(shuffledDeck));
Console.WriteLine(times);
지금까지 빌드한 코드를 실행하고 각 순서 섞기에서 데크가 다시 정렬되는 방식을 확인합니다. 8번의 순서 섞기(do-while 루프 반복) 후 데크는 시작 LINQ 쿼리에서 처음 만들었을 때의 원래 구성으로 돌아갑니다.
최적화
지금까지 빌드한 샘플은 실행할 때마다 상위 카드와 하위 카드가 동일하게 유지되는 아웃 셔플을 실행합니다. 카드 52장이 모두 위치를 바꾸는 방식인 내부 셔플을 대신 사용해 보겠습니다. 인 셔플에서는 아래쪽 절반의 첫 번째 카드가 전체 카드 더미의 첫 번째 카드가 되도록 카드를 섞습니다. 즉, 상위 절반의 마지막 카드가 하단 카드가 됩니다. 이 변경에는 한 줄의 코드가 필요합니다. 의 위치를 전환하여 현재 순서 섞기 쿼리를 업데이트합니다 TakeSkip. 이 변경은 데크의 위쪽과 아래쪽 절반의 순서를 전환합니다.
shuffledDeck = shuffledDeck.Skip(26).InterleaveSequenceWith(shuffledDeck.Take(26));
프로그램을 다시 실행하면 데크 자체의 순서를 다시 지정하는 데 52회 반복이 필요합니다. 또한 프로그램이 계속 실행됨에 따라 성능이 약간 저하됩니다.
이 성능 저하에는 몇 가지 이유가 있습니다. 지연 평가의 비효율적인 사용이라는 주요 원인 중 하나를 해결할 수 있습니다.
지연 평가는 구문의 값이 필요할 때까지 평가가 수행되지 않는다고 말합니다. LINQ 쿼리는 지연 평가되는 문장입니다. 시퀀스는 요소가 요청될 때만 생성됩니다. 일반적으로 LINQ의 주요 이점입니다. 그러나 이와 같은 프로그램에서 지연 평가로 인해 실행 시간이 기하급수적으로 증가합니다.
LINQ 쿼리를 사용하여 원래 데크를 생성했습니다. 각 순서 섞기는 이전 데크에서 세 개의 LINQ 쿼리를 수행하여 생성됩니다. 이러한 모든 쿼리는 지연적으로 수행됩니다. 즉, 시퀀스가 요청될 때마다 다시 수행됩니다. 52번째 반복에 이르면 원래 데크를 여러 번 다시 생성합니다. 이 동작을 보여 주는 로그를 작성합니다. 데이터를 수집하면 성능을 향상시킬 수 있습니다.
Extensions.cs 파일에 다음 코드 샘플에서 메서드를 입력하거나 복사합니다. 이 확장 메서드는 프로젝트 디렉터리 내에서 호출되는 debug.log 새 파일을 만들고 현재 로그 파일에 실행 중인 쿼리를 기록합니다. 이 확장 메서드를 쿼리에 추가하여 쿼리가 실행되었음을 표시합니다.
public IEnumerable<T> LogQuery(string tag)
{
// File.AppendText creates a new file if the file doesn't exist.
using (var writer = File.AppendText("debug.log"))
{
writer.WriteLine($"Executing Query {tag}");
}
return sequence;
}
다음으로, 로그 메시지를 사용하여 각 쿼리의 정의를 계측합니다.
var startingDeck = (from s in Suits().LogQuery("Suit Generation")
from r in Ranks().LogQuery("Rank Generation")
select (Suit: s, Rank: r)).LogQuery("Starting Deck");
foreach (var c in startingDeck)
{
Console.WriteLine(c);
}
Console.WriteLine();
var times = 0;
var shuffle = startingDeck;
do
{
// Out shuffle
/*
shuffle = shuffle.Take(26)
.LogQuery("Top Half")
.InterleaveSequenceWith(shuffle.Skip(26)
.LogQuery("Bottom Half"))
.LogQuery("Shuffle");
*/
// In shuffle
shuffle = shuffle.Skip(26).LogQuery("Bottom Half")
.InterleaveSequenceWith(shuffle.Take(26).LogQuery("Top Half"))
.LogQuery("Shuffle");
foreach (var c in shuffle)
{
Console.WriteLine(c);
}
times++;
Console.WriteLine(times);
} while (!startingDeck.SequenceEquals(shuffle));
Console.WriteLine(times);
유의하십시오, 쿼리에 액세스할 때마다 로그하지 않습니다. 원래 쿼리를 만들 때만 로그합니다. 프로그램을 실행하는 데는 여전히 시간이 오래 걸리지만 이제 그 이유를 확인할 수 있습니다. 로깅이 켜진 상태에서 입력 섞기를 실행하다가 인내심이 바닥나면, 다시 출력 섞기로 전환하세요. 여전히 지연 평가의 효과가 보입니다. 한 실행에서 값 및 적합 생성을 포함하여 2,592개의 쿼리를 실행합니다.
코드의 성능을 향상시켜 실행 횟수를 줄일 수 있습니다. 간단한 수정은 카드 데크를 생성하는 원래 LINQ 쿼리의 결과를 캐시 하는 것입니다. 현재 do-while 루프가 반복될 때마다 쿼리를 계속해서 실행하고, 매번 카드 덱을 다시 구성하고 셔플합니다. 카드 데크를 캐시하려면 LINQ 메서드 ToArray 를 적용하고 ToList. 쿼리에 추가하면, 이전에 지시한 동일한 작업을 실행하지만, 이제 선택한 메서드에 따라 결과를 배열 또는 목록에 저장합니다. 두 쿼리 모두에 LINQ 메서드 ToArray 를 추가하고 프로그램을 다시 실행합니다.
var startingDeck = (from s in suits().LogQuery("Suit Generation")
from r in ranks().LogQuery("Value Generation")
select new { Suit = s, Rank = r })
.LogQuery("Starting Deck")
.ToArray();
foreach (var c in startingDeck)
{
Console.WriteLine(c);
}
Console.WriteLine();
var times = 0;
var shuffle = startingDeck;
do
{
/*
shuffle = shuffle.Take(26)
.LogQuery("Top Half")
.InterleaveSequenceWith(shuffle.Skip(26).LogQuery("Bottom Half"))
.LogQuery("Shuffle")
.ToArray();
*/
shuffle = shuffle.Skip(26)
.LogQuery("Bottom Half")
.InterleaveSequenceWith(shuffle.Take(26).LogQuery("Top Half"))
.LogQuery("Shuffle")
.ToArray();
foreach (var c in shuffle)
{
Console.WriteLine(c);
}
times++;
Console.WriteLine(times);
} while (!startingDeck.SequenceEquals(shuffle));
Console.WriteLine(times);
이제 아웃 셔플은 30개의 쿼리로 줄어들었습니다. 순서 섞기를 사용하여 다시 실행하면 비슷한 개선 사항이 표시됩니다. 이제 162개의 쿼리를 실행합니다.
이 예제는 지연 평가로 인해 성능 문제가 발생할 수 있는 사용 사례를 강조 표시하도록 설계되었습니다 . 지연 평가가 코드 성능에 영향을 미칠 수 있는 위치를 확인하는 것이 중요하지만, 모든 쿼리를 열심히 실행해야 하는 것은 아니라는 점을 이해하는 것이 중요합니다.
ToArray을 사용하지 않으면 카드 덱의 새로운 배열이 이전 배열에서 구성되기 때문에 성능 저하가 발생합니다. 지연 평가를 사용하면 각 새 덱 구성이 원래 덱에서부터 빌드되며, 심지어 startingDeck을(를) 구성했던 코드를 실행하기도 합니다. 이로 인해 많은 양의 추가 작업이 발생합니다.
실제로 일부 알고리즘은 즉시 평가를 사용하여 잘 실행되고 다른 알고리즘은 지연 평가를 사용하여 잘 실행됩니다. 일상적인 사용의 경우 데이터 원본이 데이터베이스 엔진과 같은 별도의 프로세스인 경우 지연 평가가 일반적으로 더 나은 선택입니다. 데이터베이스의 경우, 지연 평가를 사용하면 더 복잡한 쿼리가 데이터베이스 프로세스에 대해 단 한 번의 왕복 요청만으로 실행되어 나머지 코드로 돌아올 수 있습니다. LINQ는 지연 또는 즉시 평가를 사용하도록 선택하든 유연하므로 프로세스를 측정하고 최상의 성능을 제공하는 평가를 선택합니다.
결론
이 프로젝트에서는 다음을 다루었습니다.
- LINQ 쿼리를 사용하여 데이터를 의미 있는 시퀀스로 집계합니다.
- LINQ 쿼리에 사용자 지정 기능을 추가하는 확장 메서드 작성
- LINQ 쿼리가 성능 저하와 같은 성능 문제가 발생할 수 있는 코드의 영역을 찾습니다.
- LINQ 쿼리에서의 지연 평가와 즉시 평가, 그리고 그에 따른 쿼리 성능에 미칠 수 있는 영향.
LINQ 외에도 마술사가 카드 트릭에 사용하는 기술에 대해 알아보았습니다. 마술사는 데크에서 모든 카드가 이동하는 위치를 제어할 수 있기 때문에 파로 셔플을 사용합니다. 이제 알다시피, 다른 사람을 위해 그것을 망치지 마십시오!
LINQ에 대한 자세한 내용은 다음을 참조하세요.
.NET