Help us understand the problem. What is going on with this article?

Unityプログラマにオススメの新しいC#の機能

Unityでも新しいC#!

長い歴史を持つプログラミング言語、C#。C#は着実に進化し、便利な言語機能を追加してきました。ところがゲームエンジンUnityでは少し前まで、古いC#しか使うことができませんでした。

2017年夏 Unity 2017.1がリリースし、「.NET 3.5 Equivalent」に加えて、「.NET 4.6 Equivalent」がExperimentalとして選べるようになりました。
2018年初夏 Unity 2018.1がリリースし、「.NET 4.x Equivalent」がExperimentalでなく、安定版になりました。
2018年冬 Unity 2018.3がリリースし、「.NET 4.x Equivalent」がデフォルトになり、「.NET 3.5 Equivalent」が非推奨になりました。

Unityも、現在は特に工夫をせずに比較的新しいC#であるC# 7.3を使うことができます。(投稿執筆時の最新C#は8.0)

ところで、Unityプログラマの方の中には「こんなC#の機能があるのか!」と驚く人や、「新しいC#の機能、わからない」と困っている人もいるのではないでしょうか?

この投稿では、Unityでのロジック記述でオススメの新しいC#の言語機能を、筆者の独断と偏見で紹介します。

プロパティの書き方いろいろ

次のコードはUnityでよく使うプロパティの例です。

ゲッターオンリーのプロパティで、SerializeFieldがついたフィールドをバッキングフィールドとしてもっています。

using System;
using System.Collections.Generic;
using UnityEngine;

[Serializable]
public class Monster
{
    [SerializeField] private int hp;

    // 古いC#でのゲッターオンリーのプロパティ
    public int Hp { get { return hp; }}
}

新しいC#では次のように、=>を使って短く書けます。

[Serializable]
public class Monster
{
    [SerializeField] private int hp;

    // 新しいC#では短く書けるゲッターオンリーのプロパティ
    public int Hp => hp;
}

冗長な部分のコードがなくなり、コードが短く簡潔になったことに注目してください。


次のコードは、古いC#におけるセッター・ゲッター両方をもつプロパティの例です。

[Serializable]
public class Monster
{
    [SerializeField] private int hp;

    // 古いC#でのセッター・ゲッタープロパティ
    public int Hp
    {
        get { return hp; }
        set { hp = value; }
    }
}

これらも=>を使って冗長な部分を取り除き、簡潔に記述することができます。

[Serializable]
public class Monster
{
    [SerializeField] private int hp;

    // 新しいC#でのセッター・ゲッタープロパティ
    public int Hp
    {
        get => hp;
        set => hp = value;
    }
}

C#にはもともと自動実装プロパティという機能がありました。
自動実装プロパティは、バッキングフィールドを自分で書かなくてよいプロパティです。

using System;

public class Player
{
    // 自動実装プロパティ
    public int Name { get; private set; }

    public Player (string name) {
        this.Name = name;
    }
}

古いC#では自動実装プロパティが使えない場面がいくつかありました。新しいC#では、自動実装プロパティが使える場面が増えています。


次のコードではreadonlyなフィールドをバッキングフィールドとしてもつNameプロパティです。
古いC#では「コンストラクタで値 or 参照を設定しそれを書き換えない」というプロパティを実現するためには、このように書く必要がありました。
自動実装プロパティは使えなかったのです。

public class Player
{
    // 古いC#では、readonlyのために自動実装プロパティでなく
    // バッキングフィールドを使う
    private readonly string name;
    public string Name { get { return name; } }

    public Player(string name)
    {
        this.name = name;
    }
}

新しいC#では、このようにreadonlyなプロパティを自動実装プロパティのみで簡潔に実現できます。

public class Player
{
    // 新しいC#では、readonlyの自動実装プロパティが使える
    public string Name { get; }

    public Player(string name)
    {
        Name = name;
    }
}

次のコードは、バッキングフィールドに初期値をフィールド初期化子で設定しているプロパティです。
古いC#ではプロパティの初期値を設定するためには、このように書く必要がありました。
自動実装プロパティは使えなかったのです。

public class Player
{
    // 古いC#では初期値を設定するために、バッキングフィールドを使う
    // 自動実装プロパティは使えない
    private  string name = "No Name";
    public string Name {
        get { return name; }
        set { name = value; }
    }
}

新しいC#では、初期値の設定とともに自動実装プロパティが使える。

public class Player
{
    // 新しいC#では初期値の設定とともに
    // 自動実装プロパティを使える
    public string Name { get; set; } = "No Name";
}

新しく加わった機能は便利機能ばかりですが、注意しないといけない機能もあります。

新しいC#では、自動実装プロパティのバッキングフィールドに属性をつけられるようになりました。この機能を使い、SerializeFieldをプロパティのバッキングフィールドに付けたくなります。

残念ながらこれは期待する挙動になりません。(フィールドの名前が変 or インスペクターに出てこない)

「自動実装プロパティのバッキングフィールドに属性付与」と「SerializeField」は合わせて使わないようにしてください。

[Serializable]
public class Monster
{

    // Unityでは使ってはいけない
    [field:SerializeField]
    public int Hp { get; }
}

新しいプロパティは、コードの設計が劇的に変わるわけではありませんが、コードが簡潔になります。ぜひ試してみてください。

複数の値を返したい時・まとめたい時はValueTuple

メソッドで複数の値を返したい時、どうすればいいでしょうか?クラスか構造体を作ればいいでしょうか?

ValueTupleは、クラスや構造体などの型を定義しなくても、複数の値をまとめることができるデータ型です。これを使えば、メソッドで複数の値を簡単に返すことができます。

ToStringや、HashCode、Equals、==での比較も実装されており、データ処理時にとても活躍します。

新しいC#では、ぜひValueTupleを使ってみてください。


ValueTupleは、非常に扱いやすい形で複数の値をまとめることができる構造体です。

ValueTupleは、つぎのように()や要素名を記述し、生成することができます。(これ以外の書き方も存在します)

var person0 = (name: "Ryota", level: 31);

上で作ったValueTupleには、namelevelというメンバがあります。

Debug.Log($"{person0.name} {person0.level}");

メソッドの返値型としてValueTupleを使う時は、このように書きます。

public static (string name, int level) LoadNameAndLevel() => (name: "Ryota", level: 31);

ToStringやHashCode、Equalsや==も実装されています。

var person0 = (name: "Ryota", level: 31);
var person1 = (name: "Ryosuke", level: 30);

Debug.Log(person0 == person1);
Debug.Log(person0.name);
Debug.Log(person0.level);
Debug.Log(person0.ToString());

ValueTupleを扱う際分解を使うと、非常に簡潔にかけます。

// ValueTupleを返すLoadNameAndLevel
public static (string name, int level) LoadNameAndLevel() => (name: "Ryota", level: 31);

public static void Main(string[] args)
{
    // 分解で返値を受け取る
    // stringのnameとintのlevel
    var (name, level) = LoadNameAndLevel();
}

今までの古いC#でも、匿名型という便利な言語機能がありました
匿名型もクラスや構造体を定義しなくても、名前のない型を作れる機能です。
詳しくはこちら「C#の匿名型について調べてみた」。
匿名型は、LINQやRxなどの処理の中間データとしては非常に便利だったのですが、メソッドの返り値型にできませんでした。
ValueTupleはメソッドの返値型にできます。

また、ValueTuple構造体よりも前、クラス型のTupleがありました。
Tupleを使えば複数の値をまとめることはできました。
しかし、メンバの名前がItem1やItem2となっていること、構造体ではなくクラスであったことなど、あまり使い安くありませんでした。


ダメージ計算・特典計算などのロジックにおいて、

「privateメソッドで複数の値をまとめて返したい。しかし型を作るほどではない」

という場面があると思います。

そのような時は、ぜひValueTupleを活用してください。

※ ValueTupleは便利ですが、型を作るべき場面もあります。使いすぎに注意してください。
※ ValueTupleを活用したライブラリ、ImportedLinqもみてみてください。

アセンブリを意識したい時のinternalとprivate protected

今までのC#のアクセスレベルは次のものがありました。

  • private
  • protected
  • internal
  • protected internal
  • public

それに加えて新しいC#では、

  • private protected

が加わりました。


UnityではAssembly Definition Filesが使えるようになり、アセンブリを意識して開発する機会が増えました。

今までのUnityにおけるアクセスレベルでは、次の3個を使うことが多かったです。

  • private
  • protected
  • public

Assembly Definition Filesにより、Unityでも簡単にアセンブリを分割できるようになりました。これにより、「アセンブリ内に閉じる」ということが大事になりました。

internalアクセス修飾子を使えば、同一アセンブリ内のみにアクセスを制限できるようになりました。Assembly Definition Filesとともに活用してください。

また、protected internalは「同一アセンブリ」もしくは「その型とその派生型」のどちらかであればアクセスできるアクセスレベルです。

新しく加わったprivate protectedは「同一アセンブリ」かつ「その型とその派生型」がアクセスできるアクセスレベルです。


新しいUnityではAssembly Definition Filesが使えるようになり、アセンブリを意識して開発する機会が増えました。

そこで、internalアクセスレベルとprivate protectedアクセスレベルを活用してください。

合わせて、「C#のアクセス修飾子 2019 〜protectedは 結構でかい〜」も参照してください。

nullの扱いもやりやすく

「null参照の発明は10億ドルにも相当する誤りだった」という言葉もありますが、C#にはnullがあります。nullと上手につきあっていかないといけません。

新しいC#では、そんなnullを上手に扱える記法が追加されています。


次のようなMonsterクラスとPlayerクラスがあります。

public class Monster
{
    public string Name { get; set; }
}

public class Player
{
    public Monster Target { get; set; }
}

MonsterのNameプロパティもPlayerのTargetプロパティもnullになりえます。

そこで次のように三項演算子とnull判定を使って、次のようなコードを書く必要があります。

本当にやりたいことは、メンバへのアクセスだけなのに、非常に冗長です。

// 古いC#では冗長
Player player = LoadPlayer();
var targetMonsterName = player != null && player.Target != null ? player.Target.Name : null;

新しいC#ではこのように?.を使って非常に簡潔に記述できます。

// 新しいC#ではこんな感じに簡潔に書ける
var targetMonsterName = player?.Target?.Name;

「もし対象がnullだったら指定した既定の値を設定したい」という状況があると思います。

古いC#では次のような書き方をする必要がありました。

// 古いC#の書き方
Player player = LoadPlayer();
var targetMonsterName = player != null && player.Target != null ? player?.Target?.Name : "Default Target Name";

新しいC#ではこのように??を使って非常に簡潔に記述できます。

// 新しいC#ではこんな感じに簡潔に書ける
var targetMonsterName = player?.Target?.Name ?? "Default Target Name";

内部的な話をすると、「player?.Target」と「player == null ? null : player.Target」は等価ではありません。==をその型が実装している時は注意してください。?.??を使う場合、==は呼ばれません。

?.??は非常に便利ですが、UnityのGameObjectやMonoBehaviourの中で使うには注意が必要です。

Unityにおいて、GameObjectやコンポーネントでは、?.??には注意が必要です。GameObjectやコンポーネントでは==が実装されています。

?.??を使った際に、何が起こるか考えてみてください。

進化したSwitch

プログラミング言語C#を学び始めた時、ほとんど全ての人はswitchを勉強したと思います。

新しいC#では、switchはとても強化されています。


今までのC#でのswitch文では、列挙型の値、数値の値、文字列の値で分岐するだけでした。

例えば次のコードのようにです。

public enum Shape
{
    Circle,
    Triangle,
    Polygon
}
public static void SwitchExample0(Shape shape)
{
    switch (shape)
    {
        case Shape.Circle:
            Debug.Log("Circleだよ");
            break;
        case Shape.Triangle:
            Debug.Log("Triangleだよ");
            break;
        case Shape.Polygon:
            Debug.Log("Polygonだよ");
            break;
        default:
            throw new ArgumentOutOfRangeException(nameof(shape), shape, "Un expected shape.");
    }
}

新しいC#では型で分岐できるようになりました。次のようなことができるようになったのです。

// objはどんな型がくるかわからない
public static void SwitchExample0(object obj)
{
    switch (obj)
    {
        case int n when n < 0:
            Debug.Log("負の数だよ!");
            break;
        case 7:
            Debug.Log("ラッキーセブンだよ!");
            break;
        case int n:
            Debug.Log($"整数だよ! {n}");
            break;
        case string s:
            Debug.Log($"文字列だよ : {s}");
            break;
        case null:
            Debug.Log("nullだよ");
            break;
        default:
            Debug.Log("それ意外だよ");
            break;
    }
}

より具体的で実用的なコードだとこのようなことができるようになりました。

public abstract class Shape
{
    public abstract double Area { get; }
}

public class Rect : Shape
{
    public int Height { get; set; }
    public int Width { get; set; }
    public override double Area => Width * Height;
}

public class Circle : Shape
{
    public int Radius { get; set; }
    public override double Area => Radius * Radius * Math.PI;
}

Shape型を継承したRect型とCircle型があります。これとswitchを使って、次のようなコードを書くことができます。

// 抽象型のShape。列挙型じゃないよ!
public static void SwitchExample0(Shape shape)
{
    switch (shape)
    {
        case Rect r when r.Width == r.Height:
            Debug.Log($"正方形だよ! 面積: {r.Area}");
            break;
        case Rect r:
            Debug.Log($"長方形だよ! 面積 : {r.Area}");
            break;
        case Circle c:
            Debug.Log($"円だよ! {c.Area}");
            break;
    }
}

ダメージ計算やポイント計算で活用できそうですね!


switchはC# 7.3のさらに先、C# 8.0でさらに進化しています。また今後のC#でさらに強くなっていくでしょう。

ダメージ計算、特定計算などで活躍すること間違いなしです。今後の強化にも期待しましょう。

構造体をより効率よく扱う

C#は、Unityそして.NET Coreの躍進により、よりいろいろな領域で活躍するようになりました。

領域が広がったことにより、パフォーマンスを求められることも増えてきました。

新しいC#では、パフォーマンス改善で活躍する多くの機能が追加されました。一例をあげると、

  • 参照ローカル変数
  • 参照戻り値
  • 読み取り専用参照
  • readonly 構造体
  • ref 構造体

などです。

これらの機能に関して、neueccさんのUnite 2019の公演、「Understanding C# Struct All Things」というとても素晴らしい公演を参照してください。

まとめ

C#は着実に進化し便利な言語機能を追加してきました。
今までUnityでは古いC#しか使えませんでしたが、最近新しいC#が使えるようになりました。
Unityプログラマの方に使って欲しい新しいC#の機能がたくさんあります!

この投稿では、Unityでのロジック記述でオススメの新しいC#の言語機能を、筆者の独断と偏見で紹介しました。

この投稿で紹介していない、便利な新しいC#の機能もたくさんあります。
次の公式ドキュメントや、ufcppさんのとてもわかりやすいブログでぜひ調べてみてください。

MSDN

ufcppさんのC# によるプログラミング入門

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away