หมายเหตุ
การเข้าถึงหน้านี้ต้องได้รับการอนุญาต คุณสามารถลอง ลงชื่อเข้าใช้หรือเปลี่ยนไดเรกทอรีได้
การเข้าถึงหน้านี้ต้องได้รับการอนุญาต คุณสามารถลองเปลี่ยนไดเรกทอรีได้
Tip
New to developing software? Start with the Get started tutorials first. They introduce classes, methods, and control flow.
Experienced in another language? This tutorial shows how C# patterns can express object behavior clearly when rules depend on the current state of an object.
In this tutorial, you build a console app that models the rules for a canal lock.
A canal lock raises or lowers boats between two stretches of water at different heights. It has two gates and a chamber whose water level changes between a low setting and a high setting. The lock can operate safely only when the water level and gate positions stay in valid combinations.
In this tutorial, you learn how to:
- Express object behavior by matching on state.
- Implement those rules with C# pattern matching.
- Use compiler diagnostics to validate your implementation.
Prerequisites
- The latest .NET SDK
- Visual Studio Code editor
- The C# DevKit
Installation instructions
On Windows, this WinGet configuration file to install all prerequisites. If you already have something installed, WinGet will skip that step.
- Download the file and double-click to run it.
- Read the license agreement, type y, and select Enter when prompted to accept.
- If you get a flashing User Account Control (UAC) prompt in your Taskbar, allow the installation to continue.
On other platforms, you need to install each of these components separately.
- Download the recommended installer from the .NET SDK download page and double-click to run it. The download page detects your platform and recommends the latest installer for your platform.
- Download the latest installer from the Visual Studio Code home page and double click to run it. That page also detects your platform and the link should be correct for your system.
- Click the "Install" button on the C# DevKit extension page. That opens Visual Studio code, and asks if you want to install or enable the extension. Select "install".
Build a simulation of a canal lock
A canal lock raises and lowers boats between waterways at different levels. In this tutorial, the simulated lock has a lower gate, an upper gate, and water that can be either low or high.
In normal operation, a boat enters when the water level inside the lock matches the level on the entry side. Once the boat is inside, both gates close. The water level changes to match the exit side, and then the exit gate opens. To keep the model safe, the water level can change only when both gates are closed, and a gate can open only when the water level matches that side.
You can model those rules with a CanalLock class. It exposes commands to open or close either gate and to raise or lower the water. It also exposes properties that report the current state of the lock.
Define the class
Create a console project, and then add a class named CanalLock. Start by designing the public API and leaving the methods unimplemented:
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}.";
}
The preceding code initializes the lock with both gates closed and the water level low. Next, add the following code to Main to guide your first implementation:
// 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}");
Now add a first implementation that changes each state value without enforcing the safety rules:
// 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;
}
These first checks pass. You have the mechanics working. Next, add a test for the first failure condition. At the end of the previous sequence, both gates are closed and the water level is low. Try to open the upper gate:
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}");
That test fails because the upper gate opens when it shouldn't. A first fix could look like this:
// 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");
}
Your tests pass again. But as you add more conditions, you accumulate more if statements. The code gets harder to scan because each rule is separated from the others.
Implement the commands with patterns
A clearer option is to use patterns to describe the valid combinations directly. In the next step, each switch expression uses one tuple as the pattern input. C# evaluates that tuple once, and each switch arm tests the current gate state, the water level, and the requested new setting.
For the upper gate, you can summarize those combinations like this:
| New setting | Gate state | Water level | Result |
|---|---|---|---|
| Closed | Closed | High | Closed |
| Closed | Closed | Low | Closed |
| Closed | Open | High | Closed |
| Open | Closed | High | Open |
| Open | Closed | Low | Closed (error) |
| Open | Open | High | Open |
The struck-through rows represent invalid internal states. The switch expression can encode the valid transitions directly. false still means the gate is closed:
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
};
Try this version. Your tests pass. The compiler also warns that the switch expression isn't exhaustive — it doesn't cover every possible value — because WaterLevel is an enum, and C# allows any value of the enum's underlying numeric type to be cast to that enum, even one without a named member. Add a final arm with the discard pattern (_) to handle those impossible internal states:
_ => throw new InvalidOperationException("Invalid internal state"),
That arm must be last because the discard pattern matches every remaining input.
You can then simplify the earlier arms. Closing the gate is always allowed, so one arm can replace the four separate closed cases:
(false, _, _) => false,
You can also combine the valid open cases and preserve the one safety error:
(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"),
Run the program again. The tests still pass. Here is the final SetHighGate implementation:
// 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"),
};
}
Implement the remaining rules
Now apply the same idea to SetLowGate and SetWaterLevel. Start by adding tests that expose invalid operations:
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}");
Run the app again. These tests fail, and the canal lock reaches invalid states. Implement the remaining methods by matching on the full state. SetLowGate is similar to SetHighGate. SetWaterLevel uses the current water level plus both gate positions:
CanalLockWaterLevel = (newLevel, CanalLockWaterLevel, LowWaterGateOpen, HighWaterGateOpen) switch
{
// arms go here
};
You have 16 combinations to consider. Start with the full table, write the switch arms, run the tests, and then simplify repeated outcomes.
Did you end up with methods similar to these?
// 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"),
};
}
Your tests should now pass, and the canal lock should enforce its safety rules.
Summary
In this tutorial, you used pattern matching to express how an object can change from one valid state to another. Patterns kept the allowed transitions together so you could compare them more easily than with a long series of branching statements. This approach works well when behavior depends on the combined shape of several state values.