Unityでも新しいC#!
長い歴史を持つプログラミング言語、C#。C#は着実に進化し、便利な言語機能を追加してきました。ところがゲームエンジンUnityでは少し前まで、古いC#しか使うことができませんでした。
そんなUnityも、現在は特に工夫をせずに比較的新しいC#を使うことができます。(投稿執筆時の最新C#は8.0、最新Unity 2019.2ではC# 7.3を利用可能です。)
ところで、Unityプログラマの方の中には「こんなC#の機能があるのか!」と驚く人や、「新しいC#の機能、わからない」と困っている人もいるのではないでしょうか?
この投稿では、Unityでのロジック記述でオススメの新しいC#の言語機能を、筆者の独断と偏見で紹介します。
プロパティの書き方いろいろ
次のコードはUnityでよく使うプロパティの例です。
ゲッターオンリーのプロパティで、SerializeField
がついたフィールドをバッキングフィールドとしてもっています。
usingSystem;usingSystem.Collections.Generic;usingUnityEngine;[Serializable]publicclassMonster{[SerializeField]privateinthp;// 古いC#でのゲッターオンリーのプロパティpublicintHp{get{returnhp;}}}
新しいC#では次のように、=>
を使って短く書けます。
[Serializable]publicclassMonster{[SerializeField]privateinthp;// 新しいC#では短く書けるゲッターオンリーのプロパティpublicintHp=>hp;}
冗長な部分のコードがなくなり、コードが短く簡潔になったことに注目してください。
次のコードは、古いC#におけるセッター・ゲッター両方をもつプロパティの例です。
[Serializable]publicclassMonster{[SerializeField]privateinthp;// 古いC#でのセッター・ゲッタープロパティpublicintHp{get{returnhp;}set{hp=value;}}}
これらも=>
を使って冗長な部分を取り除き、簡潔に記述することができます。
[Serializable]publicclassMonster{[SerializeField]privateinthp;// 新しいC#でのセッター・ゲッタープロパティpublicintHp{get=>hp;set=>hp=value;}}
C#にはもともと自動実装プロパティという機能がありました。
自動実装プロパティは、バッキングフィールドを自分で書かなくてよいプロパティです。
usingSystem;publicclassPlayer{// 自動実装プロパティpublicintName{get;privateset;}publicPlayer(stringname){this.Name=name;}}
古いC#では自動実装プロパティが使えない場面がいくつかありました。新しいC#では、自動実装プロパティが使える場面が増えています。
次のコードではreadonly
なフィールドをバッキングフィールドとしてもつNameプロパティです。
古いC#では「コンストラクタで値 or 参照を設定しそれを書き換えない」というプロパティを実現するためには、このように書く必要がありました。
自動実装プロパティは使えなかったのです。
publicclassPlayer{// 古いC#では、readonlyのために自動実装プロパティでなく// バッキングフィールドを使うprivatereadonlystringname;publicstringName{get{returnname;}}publicPlayer(stringname){this.name=name;}}
新しいC#では、このようにreadonlyなプロパティを自動実装プロパティのみで簡潔に実現できます。
publicclassPlayer{// 新しいC#では、readonlyの自動実装プロパティが使えるpublicstringName{get;}publicPlayer(stringname){Name=name;}}
次のコードは、バッキングフィールドに初期値をフィールド初期化子で設定しているプロパティです。
古いC#ではプロパティの初期値を設定するためには、このように書く必要がありました。
自動実装プロパティは使えなかったのです。
publicclassPlayer{// 古いC#では初期値を設定するために、バッキングフィールドを使う// 自動実装プロパティは使えないprivatestringname="No Name";publicstringName{get{returnname;}set{name=value;}}}
新しいC#では、初期値の設定とともに自動実装プロパティが使える。
publicclassPlayer{// 新しいC#では初期値の設定とともに// 自動実装プロパティを使えるpublicstringName{get;set;}="No Name";}
新しく加わった機能は便利機能ばかりですが、注意しないといけない機能もあります。
新しいC#では、自動実装プロパティのバッキングフィールドに属性をつけられるようになりました。この機能を使い、SerializeField
をプロパティのバッキングフィールドに付けたくなります。
残念ながらこれは期待する挙動になりません。(フィールドの名前が変 or インスペクターに出てこない)
「自動実装プロパティのバッキングフィールドに属性付与」と「SerializeField」は合わせて使わないようにしてください。
[Serializable]publicclassMonster{// Unityでは使ってはいけない[field:SerializeField]publicintHp{get;}}
新しいプロパティは、コードの設計が劇的に変わるわけではありませんが、コードが簡潔になります。ぜひ試してみてください。
複数の値を返したい時・まとめたい時はValueTuple
メソッドで複数の値を返したい時、どうすればいいでしょうか?クラスか構造体を作ればいいでしょうか?
ValueTupleは、クラスや構造体などの型を定義しなくても、複数の値をまとめることができるデータ型です。これを使えば、メソッドで複数の値を簡単に返すことができます。
ToStringや、HashCode、Equals、==
での比較も実装されており、データ処理時にとても活躍します。
新しいC#では、ぜひValueTupleを使ってみてください。
ValueTupleは、非常に扱いやすい形で複数の値をまとめることができる構造体です。
ValueTupleは、つぎのように()
や要素名を記述し、生成することができます。(これ以外の書き方も存在します)
varperson0=(name:"Ryota",level:31);
上で作ったValueTupleには、name
とlevel
というメンバがあります。
Debug.Log($"{person0.name}{person0.level}");
メソッドの返値型としてValueTupleを使う時は、このように書きます。
publicstatic(stringname,intlevel)LoadNameAndLevel()=>(name:"Ryota",level:31);
ToStringやHashCode、Equalsや==
も実装されています。
varperson0=(name:"Ryota",level:31);varperson1=(name:"Ryosuke",level:30);Debug.Log(person0==person1);Debug.Log(person0.name);Debug.Log(person0.level);Debug.Log(person0.ToString());
ValueTupleを扱う際分解
を使うと、非常に簡潔にかけます。
// ValueTupleを返すLoadNameAndLevelpublicstatic(stringname,intlevel)LoadNameAndLevel()=>(name:"Ryota",level:31);publicstaticvoidMain(string[]args){// 分解で返値を受け取る// stringのnameとintのlevelvar(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クラスがあります。
publicclassMonster{publicstringName{get;set;}}publicclassPlayer{publicMonsterTarget{get;set;}}
MonsterのNameプロパティもPlayerのTargetプロパティもnullになりえます。
そこで次のように三項演算子とnull判定を使って、次のようなコードを書く必要があります。
本当にやりたいことは、メンバへのアクセスだけなのに、非常に冗長です。
// 古いC#では冗長Playerplayer=LoadPlayer();vartargetMonsterName=player!=null&&player.Target!=null?player?.Target?.Name:null;
新しいC#ではこのように?.
を使って非常に簡潔に記述できます。
// 新しいC#ではこんな感じに簡潔に書けるvartargetMonsterName=player?.Target?.Name;
「もし対象がnullだったら指定した既定の値を設定したい」という状況があると思います。
古いC#では次のような書き方をする必要がありました。
// 古いC#の書き方Playerplayer=LoadPlayer();vartargetMonsterName=player!=null&&player.Target!=null?player?.Target?.Name:"Default Target Name";
新しいC#ではこのように??
を使って非常に簡潔に記述できます。
// 新しいC#ではこんな感じに簡潔に書けるvartargetMonsterName=player?.Target?.Name??"Default Target Name";
内部的な話をすると、「player?.Target」と「player == null ? null : player.Target」は等価ではありません。==
をその型が実装している時は注意してください。?.
や??
を使う場合、==
は呼ばれません。
?.
や??
は非常に便利ですが、UnityのGameObjectやMonoBehaviourの中で使うには注意が必要です。
Unityにおいて、GameObjectやコンポーネントでは、?.
や??
には注意が必要です。GameObjectやコンポーネントでは==
が実装されています。
?.
や??
を使った際に、何が起こるか考えてみてください。
- Reshaper/Riderの「Possible unintended bypass of lifetime check of underlying Unity engine object」って何ぞや?
- C#で「person?.Name」と「person == null ? null : person.Name」は等価じゃない。
- Possible unintended bypass of lifetime check of underlying Unity engine object (JetBrains/resharper-unityのwiki)
進化したSwitch
プログラミング言語C#を学び始めた時、ほとんど全ての人はswitchを勉強したと思います。
新しいC#では、switchはとても強化されています。
今までのC#でのswitch文では、列挙型の値、数値の値、文字列の値で分岐するだけでした。
例えば次のコードのようにです。
publicenumShape{Circle,Triangle,Polygon}
publicstaticvoidSwitchExample0(Shapeshape){switch(shape){caseShape.Circle:Debug.Log("Circleだよ");break;caseShape.Triangle:Debug.Log("Triangleだよ");break;caseShape.Polygon:Debug.Log("Polygonだよ");break;default:thrownewArgumentOutOfRangeException(nameof(shape),shape,"Un expected shape.");}}
新しいC#では型で分岐できるようになりました。次のようなことができるようになったのです。
// objはどんな型がくるかわからないpublicstaticvoidSwitchExample0(objectobj){switch(obj){caseintnwhenn<0:Debug.Log("負の数だよ!");break;case7:Debug.Log("ラッキーセブンだよ!");break;caseintn:Debug.Log($"整数だよ! {n}");break;casestrings:Debug.Log($"文字列だよ : {s}");break;casenull:Debug.Log("nullだよ");break;default:Debug.Log("それ意外だよ");break;}}
より具体的で実用的なコードだとこのようなことができるようになりました。
publicabstractclassShape{publicabstractdoubleArea{get;}}publicclassRect:Shape{publicintHeight{get;set;}publicintWidth{get;set;}publicoverridedoubleArea=>Width*Height;}publicclassCircle:Shape{publicintRadius{get;set;}publicoverridedoubleArea=>Radius*Radius*Math.PI;}
Shape型を継承したRect型とCircle型があります。これとswitchを使って、次のようなコードを書くことができます。
// 抽象型のShape。列挙型じゃないよ!publicstaticvoidSwitchExample0(Shapeshape){switch(shape){caseRectrwhenr.Width==r.Height:Debug.Log($"正方形だよ! 面積: {r.Area}");break;caseRectr:Debug.Log($"長方形だよ! 面積 : {r.Area}");break;caseCirclec: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