181
145

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

C#6.0時代のUnity

Last updated at Posted at 2017-04-16

はじめに

Unity2017のベータが公開されましたね!(これを書いている4月13日現在 Unity 2017.1.0b1 https://unity3d.com/jp/unity/beta#downloads)
なんといっても目玉は.NET 4.6が正式に対応!

覚えておいて損がないものは損が無いので覚えておきたいですし、今後.NET 4.6対応で書かれたソースコードもバンバン世に出てくるはずなので、使わないまでも読めるようになっておく必要はあるかと思います。

何ができるようになるのか、何に気を付けなくてはいけないのか。 ちょっと調べてみました。

下準備

.Net 4.6対応! とはいっても、まだ試験導入的な立ち位置なので、最初の設定では使えず、PlayerSettingsを変更する必要があります。

まず、Scripting Runtime Version
image
おお。 .NET 4.6 Equivalent がありますね!

そして、Api Compatibility Levelを.NET 4.6に・・・。
image
あるぇー?。

ってなりますが、ちゃんと再起動しろって英語で書いてありますね。
ScriptingRuntimeVersionを.Net4.6に変更して、Unityを再起動したところちゃんと4.6の項目が出てきました。
image

これで、Unity側はC#6.0相当までビルドが通ります。通ります。が、少なくとも64bit版のUnity2017から生成されるVisualStudioのslnファイルにはがっつりLangVersionに4
って書かれてしまっているので、VisualStudio側でソースコードにC#6の機能を書くと
image
と言われてエラーになってしまうっぽいです

そんなときは迷わずcsprojをメモ帳などで開いて
<LangVersion Condition=" '$(VisualStudioVersion)' != '10.0' ">4</LangVersion>
の中の46に変えてあげるとVisualStudio上でもエラーが消えます。(いつか直るのかな・・・)


2017/04/19 追記
改めて詳しく調べてみた感じだと、Visual Studio 2017 Tools for Unity がうまくないっぽいです。
https://marketplace.visualstudio.com/items?itemName=SebastienLebreton.VisualStudio2015ToolsforUnity
を改めてインストールした後、UnityのEdit→Preferences...→External Tools→External Script Editorを
image
Visual Studio 2015 にしたところ、ちゃんとしたcsprojファイルが吐き出されてました。まぁ、2017もいつかきっとなおるでしょう。
追記終了


さて。これでC#が6.0相当になりました。
いままでがC# 4.0相当だったので、新たにC#5.0の機能とC#6.0の機能が使えるようになります。

C#6.0新機能(抜粋)

  • 文字列挿入
  • オートプロパティ(拡張)
  • expression-bodied
  • null 条件演算子
  • nameof 演算子

C#5.0新機能(抜粋)

  • 非同期処理(async await)
  • Caller Info Attribute

##積極的に使っていってもよさそうなもの
###1. 文字列挿入

今までは変数の中身を含んだ文字列を生成する場合、+で連結するか、

string.Format("ほげほげ{0}です。",data);

のようにstring.Formatを使っていたところ、これからは**$** をダブルコーテーションの前に置くことで以下のように書くことができるようになります。

var str = $"ほげほげ{data}です。";

この**{~}**に式や変数を置くことで文字列に直感的に埋め込むことができるんですねー。 便利。

なお、改行や¥をエスケープせずに使いたい場合に@を使いますが、この$と一緒に使いたい場合は

var str = $@"ほげほげ
ほげ
ほげげ
{data}
ほほほげげげ\";

です。

@$"~~~~";

ではエラーになってしまうので要注意です。

###2.expression-bodied
えくすぷれっしょんぼでぃ です。かっこいい名前です。

=>を使ってメソッドやプロパティの実装が書ける感じです。

public int Data => func();

こう書くと

public int Data{
    get{
        return func();
    }
}

と書いていたのと同じになります。短くなりましたね。
もし、func()

private int cnt = 0;
private int func(){
    return cnt++;
}

のように呼び出すたびに戻り値が0→1→2→3と増えるメソッドだったとして、

//expression-bodied
public int Data => func();

void Start(){
    Debug.Log($"Data={Data}");
    Debug.Log($"Data={Data}");
    Debug.Log($"Data={Data}");
    Debug.Log($"Data={Data}");
}

とすれば、毎回Dataがgetされるたびにfuncが呼ばれるので

Data=0
Data=1
Data=2
Data=3

という出力になります。

ちなみに、

private int func(){
    return cnt++;
}

もexpression-bodiedで

private int func() => cnt++;

と書けますが、こっちはあんまり使ったことないです。

###3.Caller Info Attribute
こんなクラス(メソッド)を作れます。

public static class Log
{
    public static void DebugThrow(string message="",
        [CallerFilePath] string file = "",
        [CallerLineNumber] int line = 0,
        [CallerMemberName] string member = "")
    {
        Debug.Log($"{file}:{line}-{member}\t{message}");
    }
}

[CallerFilePath]を付けておくと、自動で呼び出し元のファイルパスがセットされます。
[CallerLineNumber]は呼び出し元の行番号
[CallerMemberName]は呼び出し元メソッドがそれぞれセットされます。

具体例としては、よくやるやつで実機でブレークが張れないような状況でよくわからないが呼ばれないメソッドがある。なんでだろう?

みたいなときに

var cnt = 0;

Debug.Log("来たよ:" + cnt++);
処理1();
Debug.Log("来たよ:" + cnt++);
処理2();
Debug.Log("来たよ:" + cnt++);
処理3();
Debug.Log("来たよ:" + cnt++);
処理4();

みたいに1行ずつDebug.Log仕込んでどこまで処理が進んだか見る。 みたいなことをやるわけです(やりません?)

これって、cntがどんどん++されていくので、20~30あると
「あれ?"来たよ:15"で処理が止まってるけど、15番目ってどれかわからん」
と、指差し確認しながら1行1行数えるハメになるのです。 つらみ。

先ほどのクラスを作っておいて

Log.DebugThrow();
処理1();
Log.DebugThrow();
処理2();
Log.DebugThrow();
処理3();
Log.DebugThrow();
処理4();

のようにLog.DebugThrow();を書くだけで、呼び出し元のファイル名、行番号、メソッド名が出力されるので、もう指差し数える日々ともおさらばです。(ステップ実行すれば良いという正論は正論です)

なお、この[CallerFilePath][CallerMemberName]アトリビュートを指定した引数にファイル名やメンバ名を指定しているのは誰かという話で。
これはプログラム実行時に動的にファイルパスやメンバ名を取得しているわけではなく、ビルド時点ですべて定数として引数に渡されるんだそうで実行時コストは特にかかっていないのだとか。素敵です。

###4.オートプロパティ(拡張)
まず、

public int Data{set;get;}


実際には

private int _data;
public int Data{
    set{
        _data = value;
    }
    get{
        return _data;
    }
}

の意味なわけで、この時点で大分省略されているわけです(自動で作成される変数名は_dataではないと思います。便宜上。ね。)
そして、このDataの初期値をセットしたい。さらに言えば外からセットされたくない。 みたいなことが多くて。
今までは

public int Data{private set;get;}
//コンストラクタ的なところ
Data = 100;

みたいにしてたわけです。
しかーし、UnityのGameObjectはコンストラクタが記述できないので、それこそ泣く泣くあえて実変数を宣言して

private int _data = 100;
public int Data{
    get{
        return _data;
    }
    private set{
        _data = value;
    }
}

とやることに。辛い歴史。(StartやAwakeに書いてもいいけれど、初期化順番などで意図しない内容になっていることが時々ある。)

それが、C#6から初期値をプロパティにもセットできるようになりました。そのため、↑の処理は

public int Data{private set;get;} = 100;

の1行に集約されます。素敵。

##使いどころが難しいもの
###1.null 条件演算子
メンバを参照する際に.を使うわけですが、?.という演算子が追加されました。

例えば、以下のようなプログラムがあったとして

class TestData{
    public int value = 100;
}

public class TestDataUser : MonoBehaviour
{
    TestData _testData;

    void Start()
    {
        Debug.Log($"testData:{_testData.value}");
    }
}

これは実行時にエラーになります。
なぜなら_testDataをセットしておらずnullなので、valueの参照に失敗するからです。よくありますね。

対処法としてはnullチェック入れようぜ。となるので

public class TestDataUser : MonoBehaviour
{
    TestData _testData;

    void Start()
    {
        if(_testData != null)
        {
            Debug.Log($"testData:{_testData.value}");
        }
        else
        {
            Debug.Log("testData:nullだよ");
        }
    }
}

なんてことをよくやりますね。
このnullチェックして、中身が入っていたら~~~というのは非常によくやります。

これをちょっと減らすことができるのが?.です。

        Debug.Log($"testData:{_testData?.value}");

こう書くと、これはエラーにならず、

testData:

と出力されます。(空白はnull。)

この?.は左辺がnullの場合には例外にはならず、結果をnullにしてくれます。
なので、今回のようにintのメンバーにアクセスしようとしている場合は、nullの可能性が出てくるのでvalueはintではなくint?ということになります。

さらに ?? 演算子を使うのが多分常套手段で

        Debug.Log($"testData:{_testData?.value.ToString() ?? "nullだよ"}");

_testDataがnullの時点で?.の機能でnullが返却され、??の左辺がnullになるので、"nullだよ"が返却される。

とか書けるわけです。

また、nullになる可能性があるので、代入式の左辺などには使えないです。

_testData?.value = 0;

これは、testDataがnullの場合はnull = 0;になっちゃうので、まぁ無理だよね。という話。

これ、個人的には非常にタイピング削減になるので良い新機能だとおもってるんですが、Unityで使うとなると話は別

これまた有名な話ではありますが、Unityの様々なクラスの基底クラスになっているObjectクラスはstatic implicit equals(Object o1,Object o2){~~}みたいな感じで等値演算子が魔改造されており、nullと比較するときだけ参照がnullかどうかをあらわすのではなく、死んでいるかどうかを表すようになってます。

なのでnull 条件演算子と話がちょっとずれますが、Unityで指定のコンポーネントが存在したらそのまま返す(例えばImage)。なかったら追加して作って返却。

というのをやりたい場合に普通にC#的に書くと

    private Image _image = null;
    public Image Image
    {
        get
        {
            return _image ?? (_image = gameObject.AddComponent<Image>());
        }
    }

なーんて書きたくなるんですが、ダメだったりするのです。
意味が違うのです。(??はEqualsメソッドと違ってオーバーロードされていない。というかできない(?))

return _image != null ? _image : (_image = gameObject.AddComponent<Image>());

が正しいことになります。
これは?.も同じで、Object継承のクラスに対しては気を付けないとnullチェックなのか、生死判定なのかで全然意味が違ってきたりして辛くなる事が予想されます。

少なくとも安心して使えて使う機会が多そうなのはActionによるイベントコールバックのような処理の場合。

    public event Action onDead;
    private void OnDestroy()
    {
        onDead?.Invoke();
    }

UnityのObjectとは無関係なので大いに使って結構です。

##使えるんだけど、今回書ききれなかったもの
###async await
超強力なんですが、coroutine(コルーチン)に慣れてしまっていたり、UniRXを使ってい るような人にわかりやすくメリットや、同居を示せる自信がないです。(TaskCompletionSource祭りになるような。)
むしろこっちが本題なのでいつか書きます。

続き書きました
Unity2017で始めるTask(async~await)

181
145
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
181
145

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?