Tip
소프트웨어 개발이 새로운가요? 먼저 시작하기 자습서부터 시작하세요. 클래스, 메서드 및 제어 흐름을 소개합니다.
다른 언어로 경험하신 적 있나요? 이 자습서에서는 규칙이 개체의 현재 상태에 따라 달라지는 경우 C# 패턴이 개체 동작을 명확하게 표현하는 방법을 보여 줍니다.
이 자습서에서는 갑문에 대한 규칙을 모델로 하는 콘솔 앱을 빌드합니다.
갑문은 서로 다른 높이에 있는 두 수역 사이에서 보트를 올리거나 내립니다. 그것은 두 개의 게이트와 수위가 낮은 수준과 높은 수준 사이에서 변하는 챔버가 있습니다. 갑문은 수위와 게이트 위치가 유효한 조합으로 유지되는 경우에만 안전하게 작동할 수 있습니다.
이 튜토리얼에서는 다음을 배우게 됩니다:
- 상태를 기준으로 매칭하여 객체의 동작을 표현합니다.
- C# 패턴 일치를 사용하여 이러한 규칙을 구현합니다.
- 컴파일러 진단을 사용하여 구현을 검증하세요.
Prerequisites
- 최신 .NET SDK
- Visual Studio Code 편집기
- C# 개발 키트
설치 지침
Windows에서 이 WinGet 구성 파일을 사용하여 모든 필수 구성 요소를 설치하십시오. 이미 설치된 항목이 있는 경우 WinGet은 해당 단계를 건너뜁니다.
- 파일을 다운로드하고 두 번 클릭하여 실행합니다.
- 사용권 계약을 읽고, y입력하고, 동의하라는 메시지가 표시되면 Enter 키를 선택합니다.
- 작업 표시줄에 깜박이는 UAC(사용자 계정 컨트롤) 프롬프트가 표시되면 설치를 계속하도록 허용합니다.
다른 플랫폼에서는 이러한 각 구성 요소를 별도로 설치해야 합니다.
- .NET SDK 다운로드 페이지 권장 설치 관리자를 다운로드하고 두 번 클릭하여 실행합니다. 다운로드 페이지에서 플랫폼을 검색하고 플랫폼에 대한 최신 설치 관리자를 권장합니다.
- Visual Studio Code 홈페이지에서 최신 설치 관리자를 다운로드하고 두 번 클릭하여 실행합니다. 그 페이지는 또한 플랫폼을 탐지하며, 링크는 시스템에 맞게 정확할 것입니다.
- C# DevKit 확장 페이지에서 "설치" 단추를 클릭합니다. 그러면 Visual Studio 코드가 열리고 확장을 설치하거나 사용하도록 설정할지 묻습니다. "설치"를 선택합니다.
운하 수문 시뮬레이션 만들기
갑문은 서로 다른 수위의 수로 사이에서 배를 올리거나 내립니다. 이 자습서에서 시뮬레이션된 수문에는 낮은 게이트, 상부 게이트 및 낮거나 높을 수 있는 물이 있습니다.
정상 작동에서는 갑문 내부의 수위가 진입측의 수위와 일치하면 보트가 들어갑니다. 보트가 내부에 들어가면 두 게이트가 모두 닫힙니다. 수위가 출구 측과 일치하도록 변한 다음, 출구 게이트가 열립니다. 모델을 안전하게 유지하기 위해 두 게이트가 모두 닫힌 경우에만 수위가 변경되고 수위가 그쪽 수위와 같을 때만 게이트가 열릴 수 있습니다.
CanalLock 클래스를 사용하여 해당 규칙을 모델링할 수 있습니다. 게이트를 열거나 닫고 물을 올리거나 낮추는 명령을 노출합니다. 또한 잠금의 현재 상태를 보고하는 속성을 노출합니다.
클래스를 정의하세요
콘솔 프로젝트를 만든 다음, CanalLock라는 이름의 클래스를 추가합니다. 먼저 공용 API를 디자인하고 메서드를 구현하지 않은 상태로 둡니다.
public enum WaterLevel
{
Low,
High
}
public class CanalLock
{
// Query canal lock state:
public WaterLevel CanalLockWaterLevel { get; private set; } = WaterLevel.Low;
public bool HighWaterGateOpen { get; private set; } = false;
public bool LowWaterGateOpen { get; private set; } = false;
// Change the upper gate.
public void SetHighGate(bool open)
{
throw new NotImplementedException();
}
// Change the lower gate.
public void SetLowGate(bool open)
{
throw new NotImplementedException();
}
// Change water level.
public void SetWaterLevel(WaterLevel newLevel)
{
throw new NotImplementedException();
}
public override string ToString() =>
$"The lower gate is {(LowWaterGateOpen ? "Open" : "Closed")}. " +
$"The upper gate is {(HighWaterGateOpen ? "Open" : "Closed")}. " +
$"The water level is {CanalLockWaterLevel}.";
}
앞의 코드는 게이트가 모두 닫히고 수위가 낮은 갑문을 초기화합니다. 다음으로, 첫 번째 구현을 안내하기 위해 다음 코드를 Main에 추가합니다.
// Create a new canal lock:
var canalGate = new CanalLock();
// State should be doors closed, water level low:
Console.WriteLine(canalGate);
canalGate.SetLowGate(open: true);
Console.WriteLine($"Open the lower gate: {canalGate}");
Console.WriteLine("Boat enters lock from lower gate");
canalGate.SetLowGate(open: false);
Console.WriteLine($"Close the lower gate: {canalGate}");
canalGate.SetWaterLevel(WaterLevel.High);
Console.WriteLine($"Raise the water level: {canalGate}");
canalGate.SetHighGate(open: true);
Console.WriteLine($"Open the higher gate: {canalGate}");
Console.WriteLine("Boat exits lock at upper gate");
Console.WriteLine("Boat enters lock from upper gate");
canalGate.SetHighGate(open: false);
Console.WriteLine($"Close the higher gate: {canalGate}");
canalGate.SetWaterLevel(WaterLevel.Low);
Console.WriteLine($"Lower the water level: {canalGate}");
canalGate.SetLowGate(open: true);
Console.WriteLine($"Open the lower gate: {canalGate}");
Console.WriteLine("Boat exits lock at upper gate");
canalGate.SetLowGate(open: false);
Console.WriteLine($"Close the lower gate: {canalGate}");
이제 안전 규칙을 적용하지 않고 각 상태 값을 변경하는 첫 번째 구현을 추가합니다.
// Change the upper gate.
public void SetHighGate(bool open)
{
HighWaterGateOpen = open;
}
// Change the lower gate.
public void SetLowGate(bool open)
{
LowWaterGateOpen = open;
}
// Change water level.
public void SetWaterLevel(WaterLevel newLevel)
{
CanalLockWaterLevel = newLevel;
}
이 첫 번째 확인은 통과했습니다. 기계 장치가 작동하고 있습니다. 다음으로, 첫 번째 실패 조건에 대한 테스트를 추가합니다. 이전 시퀀스의 끝에서 두 게이트가 모두 닫히고 수위가 낮습니다. 위쪽 게이트 열기를 시도합니다.
Console.WriteLine("=============================================");
Console.WriteLine(" Test invalid commands");
// Open "wrong" gate (2 tests)
try
{
canalGate = new CanalLock();
canalGate.SetHighGate(open: true);
}
catch (InvalidOperationException)
{
Console.WriteLine("Invalid operation: Can't open the high gate. Water is low.");
}
Console.WriteLine($"Try to open upper gate: {canalGate}");
이 테스트는 상단 게이트가 열리지 않아야 할 때 열리기 때문에 실패합니다. 첫 번째 수정 사항은 다음과 같습니다.
// Change the upper gate.
public void SetHighGate(bool open)
{
if (open && (CanalLockWaterLevel == WaterLevel.High))
HighWaterGateOpen = true;
else if (open && (CanalLockWaterLevel == WaterLevel.Low))
throw new InvalidOperationException("Cannot open high gate when the water is low");
}
테스트가 다시 통과했습니다. 그러나 조건을 더 추가하면 더 많은 if 문이 누적됩니다. 각 규칙이 다른 규칙과 분리되어 있으므로 코드를 훑어보기가 더 어려워집니다.
패턴을 사용하여 명령을 구현
더 명확한 옵션은 패턴을 사용하여 유효한 조합을 직접 설명하는 것입니다. 다음 단계에서 각 스위치 식은 하나의 튜플을 패턴 입력으로 사용합니다. C#은 해당 튜플을 한 번 평가하고 switch 식의 각 분기는 현재 게이트 상태, 수위 및 요청된 새 설정을 테스트합니다.
상단 게이트의 경우 다음과 같은 조합을 요약할 수 있습니다.
| 새 설정 | 게이트 상태 | 수위 | Result |
|---|---|---|---|
| 닫힘 상태 | 닫힘 상태 | 높음 | 닫힘 |
| 종료됨 | 닫기 | 낮음 | 닫기됨 |
| 닫힘 상태 | Open | 높음 | 닫힘 |
|
|
|
|
|
| Open | 닫기 | 높음 | Open |
| Open | 닫힌 상태 | 낮음 | 닫힘 (오류) |
| Open | Open | 높음 | Open |
|
|
|
|
|
취소선이 그어진 행은 잘못된 내부 상태를 나타냅니다. switch 식은 유효한 전환을 직접 인코딩할 수 있습니다.
false 는 여전히 게이트가 닫혀 있다는 것을 의미합니다.
HighWaterGateOpen = (open, HighWaterGateOpen, CanalLockWaterLevel) switch
{
(false, false, WaterLevel.High) => false,
(false, false, WaterLevel.Low) => false,
(false, true, WaterLevel.High) => false,
(false, true, WaterLevel.Low) => false, // should never happen
(true, false, WaterLevel.High) => true,
(true, false, WaterLevel.Low) => throw new InvalidOperationException("Cannot open high gate when the water is low"),
(true, true, WaterLevel.High) => true,
(true, true, WaterLevel.Low) => false, // should never happen
};
이 버전을 사용해 보세요. 테스트를 통과했습니다. 또한 컴파일러는 스위치 식이 완전하지 않다고 경고합니다. 즉, 가능한 모든 값을 다루지 않는다는 뜻입니다. 이는 WaterLevel이 열거형이고, C#에서는 열거형의 기본 숫자 형식의 어떤 값이든, 명명된 멤버가 없는 값이라도 해당 열거형으로 캐스팅할 수 있기 때문입니다. 불가능한 내부 상태를 처리하기 위해 무시 패턴(_)이 있는 최종 분기를 추가합니다.
_ => throw new InvalidOperationException("Invalid internal state"),
삭제 패턴이 나머지 모든 입력과 일치하기 때문에 해당 분기는 마지막이어야 합니다.
그런 다음 이전 분기를 단순화할 수 있습니다. 게이트를 닫는 것은 항상 허용되므로 한 개의 암은 4개의 개별 닫힌 케이스를 대체할 수 있습니다.
(false, _, _) => false,
유효한 열린 케이스를 결합하고 하나의 안전 오류를 유지할 수도 있습니다.
(true, _, WaterLevel.High) => true,
(true, false, WaterLevel.Low) => throw new InvalidOperationException("Cannot open high gate when the water is low"),
_ => throw new InvalidOperationException("Invalid internal state"),
프로그램을 다시 실행합니다. 테스트는 여전히 통과합니다. 여기 최종 SetHighGate 구현은 다음과 같습니다:
// Change the upper gate.
public void SetHighGate(bool open)
{
HighWaterGateOpen = (open, HighWaterGateOpen, CanalLockWaterLevel) switch
{
(false, _, _) => false,
(true, _, WaterLevel.High) => true,
(true, false, WaterLevel.Low) => throw new InvalidOperationException("Cannot open high gate when the water is low"),
_ => throw new InvalidOperationException("Invalid internal state"),
};
}
나머지 규칙을 구현하세요
이제 SetLowGate와 SetWaterLevel에 동일한 아이디어를 적용합니다. 먼저 유효하지 않은 작업을 노출하는 테스트를 추가합니다.
Console.WriteLine();
Console.WriteLine();
try
{
canalGate = new CanalLock();
canalGate.SetWaterLevel(WaterLevel.High);
canalGate.SetLowGate(open: true);
}
catch (InvalidOperationException)
{
Console.WriteLine("invalid operation: Can't open the lower gate. Water is high.");
}
Console.WriteLine($"Try to open lower gate: {canalGate}");
// change water level with gate open (2 tests)
Console.WriteLine();
Console.WriteLine();
try
{
canalGate = new CanalLock();
canalGate.SetLowGate(open: true);
canalGate.SetWaterLevel(WaterLevel.High);
}
catch (InvalidOperationException)
{
Console.WriteLine("invalid operation: Can't raise water when the lower gate is open.");
}
Console.WriteLine($"Try to raise water with lower gate open: {canalGate}");
Console.WriteLine();
Console.WriteLine();
try
{
canalGate = new CanalLock();
canalGate.SetWaterLevel(WaterLevel.High);
canalGate.SetHighGate(open: true);
canalGate.SetWaterLevel(WaterLevel.Low);
}
catch (InvalidOperationException)
{
Console.WriteLine("invalid operation: Can't lower water when the high gate is open.");
}
Console.WriteLine($"Try to lower water with high gate open: {canalGate}");
앱을 다시 실행합니다. 이러한 테스트는 실패하고 락이 잘못된 상태에 도달합니다. 전체 상태를 기준으로 매칭하여 나머지 메서드를 구현합니다. ToolStripContainer는 SplitContainer와 유사합니다.
SetWaterLevel 는 현재 수위와 두 게이트 위치를 모두 사용합니다.
CanalLockWaterLevel = (newLevel, CanalLockWaterLevel, LowWaterGateOpen, HighWaterGateOpen) switch
{
// arms go here
};
고려해야 할 16가지 조합이 있습니다. 전체 테이블로 시작하고, switch 문의 각 분기(arm)를 작성하고, 테스트를 실행한 다음, 반복되는 결과를 간소화합니다.
이러한 메서드와 비슷한 방법을 사용했나요?
// Change the lower gate.
public void SetLowGate(bool open)
{
LowWaterGateOpen = (open, LowWaterGateOpen, CanalLockWaterLevel) switch
{
(false, _, _) => false,
(true, _, WaterLevel.Low) => true,
(true, false, WaterLevel.High) => throw new InvalidOperationException("Cannot open low gate when the water is high"),
_ => throw new InvalidOperationException("Invalid internal state"),
};
}
// Change water level.
public void SetWaterLevel(WaterLevel newLevel)
{
CanalLockWaterLevel = (newLevel, CanalLockWaterLevel, LowWaterGateOpen, HighWaterGateOpen) switch
{
(WaterLevel.Low, WaterLevel.Low, true, false) => WaterLevel.Low,
(WaterLevel.High, WaterLevel.High, false, true) => WaterLevel.High,
(WaterLevel.Low, _, false, false) => WaterLevel.Low,
(WaterLevel.High, _, false, false) => WaterLevel.High,
(WaterLevel.Low, WaterLevel.High, false, true) => throw new InvalidOperationException("Cannot lower water when the high gate is open"),
(WaterLevel.High, WaterLevel.Low, true, false) => throw new InvalidOperationException("Cannot raise water when the low gate is open"),
_ => throw new InvalidOperationException("Invalid internal state"),
};
}
이제 테스트가 통과되고 canal lock이 안전 규칙을 적용해야 합니다.
Summary
이 자습서에서는 패턴 일치를 사용하여 개체가 한 유효한 상태에서 다른 유효한 상태로 변경되는 방법을 표현했습니다. 패턴은 허용되는 전환을 함께 유지하여 긴 일련의 분기문보다 더 쉽게 비교할 수 있도록 했습니다. 이 방법은 동작이 여러 상태 값의 결합된 모양에 따라 달라지는 경우에 잘 작동합니다.
.NET