C#: Null許容型とパターンマッチング
1. Null 許容値型
値型は、デフォルトではnullにできません。Nullable<T>構造体(略記:T?)を使用することで、値型にNULLセマンティクスを付与することができます。
CSHARP
int? a = 42;
int? b = null;
bool? flag = null;
double? price = 9.99;
Console.WriteLine(a.HasValue);
Console.WriteLine(b.HasValue);
Console.WriteLine(flag.GetValueOrDefault());
Console.WriteLine(price.GetValueOrDefault(0.0));
TEXT
📖 参照専用
True
False
False
9.99
▶ サンプル
CSHARP
int? score = null;
Console.WriteLine(score.HasValue);
Console.WriteLine(score.GetValueOrDefault());
Console.WriteLine(score.GetValueOrDefault(60));
score = 88;
Console.WriteLine(score.Value);
TEXT
📖 参照専用
False
0
60
88
2. ヌル結合演算子とヌル条件演算子
??はデフォルト値を指定し、?.はメンバー変数に安全にアクセスし、!はコンパイラに対して「ここではnullではない」と伝えます。
CSHARP
int? x = null;
int y = x ?? 0;
string? name = null;
int? len = name?.Length;
string s = null!;
Console.WriteLine(s is null);
TEXT
📖 参照専用
True
▶ サンプル
CSHARP
string? input = null;
string display = input ?? "(empty)";
Console.WriteLine(display);
int? length = input?.Length;
Console.WriteLine(length ?? -1);
string? city = "Beijing";
Console.WriteLine(city?.Length ?? 0);
TEXT
📖 参照専用
(empty)
-1
7
3. ヌル許容参照型
C# 8 以降では、#nullable enableを使用することで、Null 許容参照型の警告を有効にできます。string?は Null を許可し、stringは許可しません。
CSHARP
#nullable enable
string? maybeNull = null;
string notNull = "hello";
maybeNull = null;
notNull = null!;
▶ サンプル
CSHARP
#nullable enable
string Greet(string? name)
{
return name is null ? "Hello, stranger" : $"Hello, {name}";
}
Console.WriteLine(Greet(null));
Console.WriteLine(Greet("Alice"));
TEXT
📖 参照専用
Hello, stranger
Hello, Alice
4. ヌルチェックの戦略
| 戦略 | 使い方 | シナリオ |
|---|---|---|
| 明示的なチェック | if (x is null) |
分岐ロジックが必要 |
| ヌルコアリシング | x ?? defaultValue |
デフォルト値の指定 |
| ヌル条件 | x?.Member |
安全なメンバーアクセス |
▶ サンプル
CSHARP
string? GetName() => null;
string? name = GetName();
if (name is null)
{
Console.WriteLine("Name is null");
}
string upper = name?.ToUpper() ?? "DEFAULT";
Console.WriteLine(upper);
TEXT
📖 参照専用
Name is null
DEFAULT
5. パターンマッチングの概要
パターンマッチングは、型チェックと変数抽出を一つに統合したもので、一般的にisやswitchとともに使用されます。
(1) はパターンです
CSHARP
object obj = 42;
if (obj is int i)
{
Console.WriteLine($"Is integer: {i}");
}
TEXT
📖 参照専用
Is integer: 42
▶ サンプル
CSHARP
object value = "hello";
if (value is int n)
{
Console.WriteLine(n);
}
else if (value is string s)
{
Console.WriteLine($"String length: {s.Length}");
}
TEXT
📖 参照専用
String length: 5
6. 型パターンとswitch文
switchでタイプ別に分岐し、変数を抽出します。
▶ サンプル
CSHARP
string Describe(object obj)
{
return obj switch
{
int i => $"Integer {i}",
double d => $"Double {d}",
string s => $"String \"{s}\"",
bool b => $"Boolean {b}",
null => "null",
_ => "Unknown type"
};
}
Console.WriteLine(Describe(10));
Console.WriteLine(Describe(3.14));
Console.WriteLine(Describe("hi"));
Console.WriteLine(Describe(true));
TEXT
📖 参照専用
Integer 10
Double 3.14
String "hi"
Boolean True
7. プロパティ・パターン
プロパティパターンでは、比較のために手動で値を抽出することなく、オブジェクトのプロパティに基づいて照合が行われます。
▶ サンプル
CSHARP
var person = new { Name = "Alice", Age = 20 };
if (person is { Age: > 18 })
{
Console.WriteLine("Adult");
}
string Category(object p) => p switch
{
{ Age: < 12 } => "Child",
{ Age: >= 12 and < 18 } => "Teenager",
{ Age: >= 18 } => "Adult",
_ => "Unknown"
};
Console.WriteLine(Category(person));
TEXT
📖 参照専用
Adult
Adult
8. レコードタイプ
C# 9 では record が導入され、デフォルトで値ベースの等価比較と不変性が提供されるようになりました。
CSHARP
record Person(string Name, int Age);
▶ サンプル
CSHARP
record Person(string Name, int Age);
var p1 = new Person("Alice", 18);
var p2 = new Person("Alice", 18);
Console.WriteLine(p1 == p2);
Console.WriteLine(ReferenceEquals(p1, p2));
Console.WriteLine(p1);
TEXT
📖 参照専用
True
False
Person { Name = Alice, Age = 18 }
9. 式を使用する場合
withは、既存レコードのコピーを作成し、一部のプロパティを変更しますが、元のインスタンスは変更されません。
▶ サンプル
CSHARP
public class Student : IEquatable<Student>
{
public string Name { get; }
public int Age { get; }
public string Grade { get; }
public Student(string name, int age, string grade)
{
Name = name;
Age = age;
Grade = grade;
}
public Student With(int? age = null, string grade = null)
{
return new Student(Name, age ?? this.Age, grade ?? this.Grade);
}
public bool Equals(Student other)
{
return other != null && Name == other.Name && Age == other.Age && Grade == other.Grade;
}
public override bool Equals(object obj) => Equals(obj as Student);
public override int GetHashCode() => System.HashCode.Combine(Name, Age, Grade);
public override string ToString() => $"Student {{ Name = {Name}, Age = {Age}, Grade = {Grade} }}";
}
class Program
{
static void Main()
{
var s1 = new Student("Bob", 20, "A");
var s2 = s1.With(age: 21, grade: "A+");
System.Console.WriteLine(s1);
System.Console.WriteLine(s2);
}
}
TEXT
📖 参照専用
Student { Name = Bob, Age = 20, Grade = A }
Student { Name = Bob, Age = 21, Grade = A+ }
10. タプル
タプルは複数の値を軽量な構造体にまとめ、名前付き要素や展開をサポートしています。
▶ サンプル
CSHARP
(int Id, string Name) person = (1, "Charlie");
Console.WriteLine(person.Id);
Console.WriteLine(person.Name);
var coords = (X: 3.0, Y: 4.0);
double distance = Math.Sqrt(coords.X * coords.X + coords.Y * coords.Y);
Console.WriteLine(distance);
(int x, int y) = (10, 20);
Console.WriteLine($"x={x}, y={y}");
TEXT
📖 参照専用
1
Charlie
5
x=10, y=20
11. タプルの展開とパターンマッチング
タプルとパターンマッチングを組み合わせることで、複数の条件を含む分岐を洗練された方法で処理できます。
▶ サンプル
CSHARP
string Classify(int score, bool hasBonus) => (score, hasBonus) switch
{
(>= 90, true) => "Excellent+Bonus",
(>= 90, false) => "Excellent",
(>= 60, true) => "Pass+Bonus",
(>= 60, false) => "Pass",
_ => "Fail"
};
Console.WriteLine(Classify(95, true));
Console.WriteLine(Classify(75, false));
Console.WriteLine(Classify(50, true));
TEXT
📖 参照専用
Excellent+Bonus
Pass
Fail
❓ よくある質問
Q
int?やintを算術演算に直接使用できますか?A いいえ。演算を行う前に、
.Valueまたは??を使用して、nullではない値に変換する必要があります。Q
string?とstringの違いは何ですか?A
string?ではnullの代入が可能ですが、stringでは、nullableが有効な状態でnullを代入するとコンパイラ警告が発生します。Q レコードとクラスの根本的な違いは何ですか?
A レコードは値による等価比較(== で内容を比較)を使用するのに対し、クラスは参照による等価比較(== でメモリアドレスを比較)を使用します。
Q
with 式は class と一緒に使用できますか?A いいえ。
with は、レコード型、または初期化のみのプロパティを持つ構造体でのみ使用できます。Q タプルの要素数に制限はありますか?
A ValueTuple は最大 8 つの型パラメータをサポートしています。それ以上になる場合は、拡張のためにネストされた TRest が使用されます。
📖 まとめ
T?はNullable<T>であり、値型がnullになることを許可していますHasValue/Value/GetValueOrDefault()ヌル許容値へのアクセス??、?.、!は、NULLが発生するケースに対応しています#nullable enableヌル許容参照型のチェックを有効にするisパターン、型パターン、プロパティパターンは、型チェックを簡素化するswitchパターンマッチングを用いた式は、多岐分岐のロジックを簡潔に表現できるrecordは、値の等価性と不変性を提供しますwithは、一部の属性を変更したレコードのコピーを作成します- タプルは複数の値を軽量に組み合わせることができ、命名や展開をサポートしています
📝 練習問題
double? price = null;を宣言し、GetValueOrDefault(99.9)を使用してデフォルト値を取得し、それを出力するstring?というパラメータを受け取り、?.と??を使用して文字列の長さを返す、あるいは-1を返すメソッドを作成してください。isというパターンを使って、object obj = 3.14;がdouble dであるかどうかを確認し、d * 2を出力するrecord Book(string Title, double Price);を定義し、インスタンスを作成し、withを使用して価格を変更する- タプルスイッチ式を使用して、
(int month, int day)が「元旦」か「国慶節」かを判定する