📗

[Unity]覚えとくと少しだけスマートになるC#の機能

2024/12/12に公開

はじめに

初めての方も、そうでない方もこんにちは!
現役ゲームプログラマーのたむぼーです。
自己紹介を載せているので、気になる方は見ていただければ嬉しいです!

今回は
 覚えとくと少しだけスマートになるC#の機能
について紹介します

https://zenn.dev/tmb/articles/1072f8ea010299

補足

今から紹介する機能を使うためには、C#のバージョンを確認する必要があります。

C#のリリースノート

今回は一部の紹介なので、もっと詳しく知りたい方をこちらを見てみるといいです!
https://learn.microsoft.com/ja-jp/dotnet/csharp/whats-new/csharp-version-history

UnityのC#バージョン表

Unityバージョン リリース日 C#バージョン
2021.3 LTS 2024/12/11 9.0
2022.3 LTS 2024/12/4 9.0
2023.2 2024/4/25 9.0
6000.0(Unity 6) 22024/12/4 9.0

Unityバージョンが2021.3 LTS以上なら、以下の機能を使うことができます
(サポートされてない機能もあるので、詳しくはこちらを見てください!)

C# 8.0でできること

switch式

switch式

■適当なenumを作る

/// <summary>
/// 適当なenumを作る
/// </summary>
public enum Type
{
    A,
    B,
    C
}

■従来の書き方
case-breakかcase-returnする必要あった。

/// <summary>
/// 従来の書き方
/// </summary>
public string GetTypeDescription(Type type)
{
    switc(type)
    {
        case Type.A: return "Type: A";
        case Type.B: return "Type: B";
        case Type.C: return "Type: C";
        default: return "Unknown Type";
    };
}

■C# 8.0以降でできるswitch式
直接返却できるよ!

/// <summary>
/// switch文を簡潔にできるようになったよ!
/// </summary>
public string GetTypeDescription(Type type)
{
    return type switch
    {
        Type.A => "Type: A",
        Type.B => "Type: B",
        Type.C => "Type: C",
        _ => "Unknown Type"
    };
}

■C# 8.0以降でできるswitch式(ラムダ式)

/// <summary>
/// ラムダ式でもできるよ!
/// </summary>
public string GetTypeDescription(Type type) => type switch
{
    Type.A => "Type: A",
    Type.B => "Type: B",
    Type.C => "Type: C",
    _ => "Unknown Type"
};

Null許容参照型

Null許容参照型

参照型のnullの扱いを明示できるようになったよ!
nullを許容するかしないかを明示的にすることで、コードの意図が伝わりやすくなるよ!
nullを許容する(?)を使うためには、#nullable enable ~~ #nullable disableの記述が必要です
#nullable enableの記述がないと、nullを許容する(?)は意味を持たなくなります!

/// <summary>
/// string?でnullが入る可能性を明示的にできるよ
/// 全体nullが入らないって保証付きなら?は不要
/// </summary>
#nullable enable // 以下の行はnull許容参照型を有効
public int GetLength(string? text)
{
	// 引数を見ただけで、
	// あ、もしかしたらnullが入るかもしれないんだ!
	// それなら、nullチェックしとこ~^^
	// って解釈ができるようになる

	int length = 0;
	if (text != null)
	{
		length = text.Length;
	}
	return length;
}
#nullable disable // 以下の行はnull許容参照型を無効

インデックスと範囲

インデックスと範囲

配列やコレクションを簡単に操作できるようになったよ
インデックス構文(^)と範囲構文(..)が追加!
■適当な配列

int[] array = new int[] { 11, 22, 33, 44, 55 };

■ インデックス構文(^)について

// イメージ的には、こう
// array[^N] == array[array.Length - N]

// 最後の要素: 55
int last = array[^1];

// 最後から2番目の要素: 44
int secondLast  = array[^2];

// エラー: System.IndexOutOfRangeException
int invalidLast = array[^6];

■範囲構文(..)について

// イメージ的には、こう
// array[N1..N2] == for (int i = N1; i < N2; i++)
// 開始インデックス..終了インデックス(終了インデックスは含まない)

// 範囲(1~3): { 22, 33, 44 }
int[] subArray = array[1..4];

// 最初の3要素(0~2): { 11, 22, 33 }
int[] firstArray = array[..3];

// 最後の3要素(2~4): { 33, 44, 55 }
int[] lastArray = array[2..];

// Indexが5以降は存在しないので、空の配列になる
int[] emptyArray = array[5..];

■ちなみに、変数にもできるよ!

int lastIndex = 2;

// 最後から2番目の要素: 44
int secondLast  = array[^lastIndex];

int endIndex = 3;

// 最初の3要素(0~2): { 11, 22, 33 }
int[] firstArray2 = array[..endIndex]; 

■範囲構文についての補足

// array[1..4];をforで表すと、こんな感じ!
int startIndex = 1;
int endIndex = 4;
int[] targetArray = new int[endIndex]; // 最終結果を格納する変数
int targetIndex = 0; // targetArrayのIndex
for (int i = startIndex; i < endIndex; i++, targetIndex++)
{
    // 範囲(1~3): { 22, 33, 44 }
    // targetArray[0] = array[1]; -> 22
    // targetArray[1] = array[2]; -> 33
    // targetArray[2] = array[3]; -> 44
    // このようになる!
    targetArray[targetIndex] = array[i];
}

null合体演算子

null合体演算子

nullチェックをして、nullだったら代入する

private object _obj = null;

/// <summary>
/// 従来の方法
/// </summary>
public void Create()
{
    // まだ作成されてない(null)なら、作成する
    if (_obj == null)
    {
        _obj = new object();
    }
}

■C# 8.0以降でできるnull合体代入

private object _obj = null;

/// <summary>
/// ??= は、左辺(_obj)が null の場合のみ代入を行う
/// </summary>
public void Create()
{
    // まだ作成されてない(null)なら、作成する
    _obj ??= new object();

    // もう一度同じように書いた場合、
    // _objはnullじゃないので、代入しない!
    _obj ??= new object();
}

ストリーム文字列補間

ストリーム文字列補間

文字列補間($)と逐語的文字列リテラル(@)を組み合わせた構文ができるよ
■文字列補間($)の例

// $をつけることで、{}で値を動的に挿入できるよ
int applePrice = 120;
string priceText = $"りんごの値段は{applePrice}円です!"

■逐語的文字列リテラルの例

// @をつけることで、バックスラッシュ (\) などをエスケープする必要がないよ
string path1 = "C:\\Users\\UserName\\Documents"; // @つけないとこうなる
string path2 = @"C:\Users\UserName\Documents"; // @つけるとそのままいける

■文字列補間と逐語的文字列リテラルを組み合わせた例

// $と@をつけると、こんな感じ!(一般的にはこっち)
string path1 = $@"C:\Users\{name}\Documents";

// ちなみに、$と@の順番はどちらが先でもいいよ!
string path2 = @$"C:\Users\{name}\Documents";

C# 9.0でできること

record型

record型

主にデータの保持だけで、計算などロジックをあまり持たない場合に使用します
record型を使うことで、簡潔にイミュータブル(変更不可)で内容ベースの比較(Equals や == のオーバーライド)を扱うことができます!

/// <summary>
/// classの場合 - 価格モデル
/// </summary>
public class PriceModel
{
    // イミュータブル(変更不可)にするために、private setにする
    public string Name { get; private set; }
    public int Quantity { get; private set; }
    public int Price { get; private set; }

    public PriceModel(string name, int quantity, int price)
    {
        Name = name;
        Quantity = quantity;
        Price = price;
    }
}

/// <summary>
/// こんな感じで使うよ!
/// </summary>
public void SetPrice()
{
    var apple1 = new PriceModel("りんご", 1, 120);
    var apple2 = new PriceModel("(特売)りんご", 3, 300);
    var banana = new PriceModel("バナナ", 3, 200);
    var potato = new PriceModel("じゃがいも", 6, 399);

    Debug.Log(apple1.Name); // "りんご"
    Debug.Log(apple2.Name);// "(特売)りんご"
    // apple1.Name = "アイフォン"; // 変更できないよ(コンパイルエラー)

    // 同じ商品かチェック
    // これはfalse
    // classの場合、参照の等価性を比較(つまり、同じメモリ参照を持っているかどうか)
    if (apple1 == new PriceModel("りんご", 1, 120))
    {
        Debug.Log("同じ商品です!");
    }
}

■record型だと・・・?

/// <summary>
/// 価格モデル
/// </summary>
public record PriceModel(string Name, int Quantity, int Price);

/// <summary>
/// こんな感じで使うよ!
/// </summary>
public void SetPrice()
{
    // 使い方はclassと同じ
    var apple1 = new PriceModel("りんご", 1, 120);
    var apple2 = new PriceModel("(特売)りんご", 3, 300);
    var banana = new PriceModel("バナナ", 3, 200);
    var potato = new PriceModel("じゃがいも", 6, 399);

    Debug.Log(apple1.Name); // "りんご"
    Debug.Log(apple2.Name);// "(特売)りんご"
    // apple1.Name = "アイフォン"; // 変更できないよ(コンパイルエラー)

    // 同じ商品かチェック
    // これはtrue
    // record型は、自動で内容ベースの比較が実装されるので、可能
    if (apple1 == new PriceModel("りんご", 1, 120))
    {
        Debug.Log("同じ商品です!");
    }
}

record型の非破壊的デコンストラクション

record型の非破壊的デコンストラクション

■使用するrecord型の例

/// <summary>
/// 価格モデル
/// </summary>
public record PriceModel(string Name, int Quantity, int Price);

■一般的な使い方

PriceModel apple = new PriceModel("りんご", 1, 120);
Debug.Log($"名前: {apple.Name}, 個数: {apple.Quantity}, 価格: {apple.Price}");

■デコンストラクションの使い方

PriceModel apple = new PriceModel("りんご", 1, 120);
var (name, quantity, price) = apple;
Debug.Log($"名前: {name}, 個数: {quantity}, 価格: {price}");

■デコンストラクションの使い方(関数の戻り値)

/// <summary>
/// PriceModelを返す関数
/// </summary>
public PriceModel GetPriceModel()
{
    return new PriceModel("りんご", 1, 120);
}

/// <summary>
/// 関数の戻り値として使用する例
/// </summary>
public void UseDeconstruction()
{
    var (name, quantity, price) = GetPriceModel();
    Debug.Log($"名前: {name}, 個数: {quantity}, 価格: {price}");
}

■デコンストラクションの使い方(パターンマッチングで条件分岐を簡略化)

public void CheckPrice(PriceModel price)
{
    if (price is PriceModel("りんご", int quantity, int price) && price > 10000)
    {
        Debug.Log($"高級りんごです。価格:{price}}");
    }
    else if (price is PriceModel("りんご", int quantity, int price))
    {
        Debug.Log($"りんごです。価格:{price}}");
    }
    else if (price is PriceModel("アイフォン", int quantity, int price))
    {
        Debug.Log($"これは、アイフォンですね。価格:{price}}");
    }
}

■デコンストラクションの使い方(データの一部を使う)
※デコンストラクションの順序が正しくないと、正しい値が取得できません!

PriceModel apple = new PriceModel("りんご", 1, 120);

// 必要なプロパティだけ取得(nameとpriceを取得)
var (name, _, price) = apple;
// var (name, price, _) = apple; // 順番が違うよ!コンストラクタの順序でないとダメ

// 必要なプロパティだけ取得(nameのみ取得)
var (name, _) = apple;

Console.WriteLine($"これは{name}です。価格は{price}です!");

with式

with式

record型やinitプロパティで作成されたオブジェクトを部分的にコピー

/// <summary>
/// 価格モデル
/// </summary>
public record PriceModel(string Name, int Quantity, int Price);

var apple = new PriceModel("りんご", 1, 120);
var updateApple = apple with { Name = "アイフォン", Price = 150000 };

パターンマッチングの強化

パターンマッチングの強化

■論理パターン
and, or, not を使って条件を組み合わせ可能

public string CheckNumber(int num) => num switch
{
    > 0 and < 10 => "1から9の間です",
    > 10 or < 0 => "範囲外だよ~",
    not 0 => "0以外だよ",
    _ => "0だよ"
};

■ 型パターンの簡略化

if (obj is PriceModel priceModel)
{
    Console.WriteLine($"この商品は・・・{priceModel.Name}でした!");
}

■列挙パターン
比較演算子を直接使用できるよ

public string GetGrade(int score) => score switch
{
    >= 90 => "スコアはAです!",
    >= 80 => "スコアはBです!",
    >= 70 => "スコアはCです!",
    _ => "スコアはFです!"
};

■record型との組み合わせ

/// <summary>
/// 価格モデル
/// </summary>
public record PriceModel(string Name, int Quantity, int Price);

public string CheckPrice(PriceModel price) => price switch
{
    { Name: "りんご", Price: > 1000 } => "高級リンゴです。",
    { Name: "アイフォン" } => "これはアイフォンですね。",
    _ => "普通の商品です。"
};

■record型との組み合わせ
ネストした条件をまとめることが可能になります

public bool IsLuxury(PriceModel priceModel) => priceModel.Price switch
{
    >= 10000 => true,
    _ => false
};

Init-Onlyプロパティ(Unityサポート外)

Init-Onlyプロパティ
/// <summary>
/// 価格モデル
/// </summary>
public class PriceModel
{
    public string Name { get; init; }
    public int Quantity { get; init; }
    public int Price { get; init; }
}

PriceModel apple = new PriceModel
{
    Name = "りんご",
    Quantity = 1,
    Price = 120
};

// person.Name = "アイフォン"; // initプロパティは変更不可(コンパイルエラー)

Discussion