C#最新バージョンの新機能を簡単に解説!(C#8.0~C#9.0)

みなさんはC#にどんな機能があるか把握しているでしょうか?
C#は頻繁に機能が追加されており、日々便利になっています。

しかしその分、2021年1月現在のC#最新バージョン(C#9.0)でどんな機能が使えるのかご存じない方も多いでしょう。

そこでこの記事は、比較的新しいバージョンである「C#8.0からC#9.0」を対象に、実際によく使う機能を簡単なサンプルを紹介しながら解説します。

◆Visual Studioの設定方法
◆C#9.0 レコード(record)型
◆C#9.0 init専用セッター
◆C#8.0 インデックスと範囲
◆C#8.0 パターンマッチング

対応するVisual Studioのバージョンと.Net Coreのバージョンの設定

本記事ではC#8.0からC#9.0を対象にしています。全ての機能を試したい場合、「Visual Studio 2019」を使用し、「.Net 5.0」をフレームワークとして指定する必要があります。

Visual Studioのメニューから「プロジェクト」→「~のプロパティ」→「アプリケーション」→「対象のフレームワーク」から「.Net 5.0」を指定してください。

今回のサンプルは「.NET 5.0」をフレームワークとして指定する必要があります。

サンプルコード

    public record Animal
    {
        public string Name { get; init; }
        public Animal(string name)
        {
            if(name.Contains(":"))
            {
                Name = name[(name.IndexOf(":") + 1)..];
            }
            else
            {
                Name = name;
            }
        }
        public static Animal Parse(string name)
        {
            return new Animal(name);
        }
        public override string ToString()
        {
            return $"{GetType().Name}:{Name}";
        }
    }

    public record Dog : Animal
    {
        public bool IsPet { get; init; } = true;
        public Dog()
            : this(string.Empty) { }
        public Dog(string name)
            : this(name, false) { }
        public Dog(string name, bool isPet)
            : base(name) => IsPet = isPet;
        public static new Dog Parse(string str)
        {
            var data = from d in str.Split(",")
                        select d.Trim();
            return data.Count() switch
            {
                1 => new Dog(data.ElementAt(0)),
                2 => new Dog(
                        data.ElementAt(0), 
                        bool.Parse(data.ElementAt(1))
                    ),
                _ => new Dog(),
            };
        }
        public override string ToString()
        {
            return $"{base.ToString()},{IsPet}";
        }
    }

C#9.0で使用できる機能

レコード(record)型

レコード(record)型は、「定義後に変更できない、参照型のオブジェクトを定義する」ための機能です。これまでC#で利用できる型は大きく分けて、以下の2つに大別されました(参考*1)。

・参照型(クラスなど)
・値型(構造体、列挙型など)

レコード型はこの2つの中間のようなもので、以下3つの特徴を持っています。

・参照型であり継承などを行える
・値型のようにあとから変更ができない
・比較などに特別な考慮が不要

要は、レコード型はクラスのような機能を持ちつつ、「自動的に等価判定用のコードが定義される」という振る舞いをします。等価判定用のコードとは、具体的に「Equals」メソッドと「GetHashCode」メソッドをオーバーライドすることです。

サンプルのAnimal.ParseやDog.Parseメソッドを見てください。どちらも内部でnewされているため、これがもし通常のクラスを用いて定義されていた場合、「Equals」メソッドや比較演算子(==)を用いても同一として判定できません。

対してサンプルのようにレコード型を用いていれば、問題なく同一として判定できます。

    public class AnimalClass
    {
                    : // 省略
        public static AnimalClass Parse(string str)
        {
            // 省略
        }
    }
    public class DogClass : AnimalClass
    {
                    : // 省略
        public static new DogClass Parse(string str)
        {
            // 省略
        }
    }
 var dog_r = new Dog { Name = "John", IsPet = false };
        var dogp_r = Dog.Parse(dog_r.ToString());
        Console.WriteLine(dog_r == dogp_r);

※ レコード型を用いた例。Console.WriteLineの結果は「True」。

        var dog_c = new DogClass
        {
            Name = "John",
            IsPet = false
        };
        var dogp_c = DogClass.Parse(dog_c.ToString());
        Console.WriteLine(dog_c == dogp_c);

※ クラスを用いた例。Console.WriteLineの結果は「False」。

init専用セッター

init専用セッター(init only setter)は、プロパティ定義において、「初期化時にのみ値が更新されることを明示できるアクセサー」です。

コード内部にデータを定義する際、往々にして「protected set{~}」や「private set{~}」などのプロパティのアクセサを用いて、意図しない値の変更が行えないようカプセル化することが常でした。しかしこれでは「どのタイミングで更新されるのか」までは分からず、結果としてコードを見直す際に手間がかかるという問題がありました。

しかし、init専用セッターを用いればこの問題は解決できます。なぜならinit専用セッターを用いれば、プロパティを変更できるタイミングが以下に示す範囲に限定されるからです(参考*2)。

・オブジェクト初期化子の実行中
・with式初期化子の実行中
・またはの派生型のインスタンスコンストラクター内で、 this または base
・init任意のプロパティのアクセサー内で、 this またはbase
・名前付きパラメーターを使用した属性の使用

このinit専用セッターはレコード型との相性が高いばかりか、通常のクラス定義にも織り交ぜることができます。バグを減らすための機能として、便利に使っていきましょう。

C#8.0で使用できる機能

インデックスと範囲

C#8.0より、インデックスと範囲の指定による値の取り出しが楽に指定できるようになりました。

以下のコードは、nameで指定された変数に含まれる「:」記号以降のみを切り出します。

Name = name[(name.IndexOf(":") + 1)..];

これは従来の「string.Substring」メソッドで置き換えた、以下の内容と同義です。

Name = name.Substring(name.IndexOf(":") + 1);

この指定は配列や文字列型などの「インデクサ」(indexer)や「レンジ」(Range, 範囲)などのシーケンスを実装している場合に使用できます。

このインデックスと範囲による記法は簡単です。

具体的には「..」より前に指定された位置(インデックス)から、「..」より後に指定された「長さ分の要素」を取り出します。この際、開始位置を省略すると先頭から、長さを省略すると残りの要素長が指定されたとみなされます。また開始位置の指定で数値の前に「^」を指定すると、後方から(LastIndexOf)指定したものとみなされます。

これは覚えてしまえば色々な箇所で利用できるため、ぜひとも身に着けたい記法です(参考*3)。

パターンマッチング

C#8.0から、パターンマッチングの機能が強化されました。サンプルではいくつかあるパターンマッチング機能から、「switch」構文の例を示しています(参考*4)。

            return data.Count() switch
            {
                1 => new Dog(data.ElementAt(0)),
                2 => new Dog(data.ElementAt(0), bool.Parse(data.ElementAt(1))),
                _ => new Dog(),
            };

このサンプルでは、配列(data)の個数によって呼び出すコンストラクタを変更(配列の個数に対して、パターンを決定=パターンマッチング)しています。

通常のswitch~case文で記述するより、とてもシンプルに記述できます。

            switch (data.Count())
            {
                case 1: return new Dog(data.ElementAt(0));
                case 2: return new Dog(
                    data.ElementAt(0), 
                    bool.Parse(data.ElementAt(1))
                );
                default: return new Dog();
            };

おわりに

今回のサンプルは、できるだけ多くのC#の最新の記法を取り入れて作成しました。C#6.0~C#9.0にとどまらず、C#3.0の「ラムダ記法」や「LINQ」に「var」による型推論を用いたり、C#6.0の「文字列補完」なども用いています。加えて、もちろんC#にはこの他にも様々な便利な機能が実装されています(参考*5)。

本記事はもとより、皆さんが参考文献に示した資料を読み漁り、コーディングを楽にするノウハウを取り入れていく一助になることを願っております。




エンジニア募集中

株式会社キャパでは中途エンジニアの募集をしています。
私たちと一緒に働きませんか?
募集概要について詳しくは


◆参考文献
参考*1
レコード型
https://docs.microsoft.com/ja-jp/dotnet/csharp/whats-new/csharp-9#record-types

参考*2
init専用セッター
https://docs.microsoft.com/ja-jp/dotnet/csharp/whats-new/csharp-9#init-only-setters

参考*3
インデックスと範囲
https://docs.microsoft.com/ja-jp/dotnet/csharp/whats-new/csharp-8#indices-and-ranges

参考*4
パターンマッチング:switch式
https://docs.microsoft.com/ja-jp/dotnet/csharp/whats-new/csharp-8#switch-expressions

参考*5
C#の歴史
https://docs.microsoft.com/ja-jp/dotnet/csharp/whats-new/csharp-version-history

関連記事一覧