【C#】自動実装プロパティの話
はじめに
この記事は C# その2 Advent Calendar 2018 の十四日目の記事です。
普段何気なく使っているプロパティですが、中のデータはどのように扱われているのかな~、と思ったので調べてみることにしました。
特に自動実装プロパティを中心に見てみたいと思います。
自動実装プロパティについて
まずは今回の話の中心となる自動実装プロパティについて軽くまとめます。
具体的にはこれです。
public string Name { get; set; }
これによっていちいち手動で Getter 、 Setter メソッドを用意しなくとも、 Name = ""; のような処理では Setter の、 string n = Name; のような処理では Getter の役割を果たしてくれる、というものです。
特に興味を持ったのは、外部クラスからアクセスした場合は Getter 、 Setter メソッドとしてふるまうが、プロパティを定義しているクラス自身からアクセスした場合は変数としてふるまう、というところでした。
(うろ覚え)
実際どうなの? ということで、さっそく確認用にあれこれ用意してみました。
using System; namespace ConsoleApp1 { class SampleClass { public string Name { get; set; } private string _name = "world"; public string GetName() { return _name; } public void SetName(string value) { _name = value; } public string GetOnlyName { get; } public string GetName2 => _name; public string GetName3 => Name; public string GetName4 => "world"; public string Name5 { get; set; } = "hello world"; public SampleClass() { Name = "hello"; _name = "hello"; SetName("hello"); GetOnlyName = "hello"; } public void CallSample() { Console.WriteLine("N1 " + Name); Console.WriteLine("N2 " + _name); Console.WriteLine("N3 " + GetName()); Console.WriteLine("N4 " + GetOnlyName); Console.WriteLine("N5 " + GetName2); Console.WriteLine("N6 " + GetName3); Console.WriteLine("N7 " + GetName4); Console.WriteLine("N8 " + Name5); } } }
自動実装プロパティの他、 C# 6 で追加された Get-Only プロパティ、ラムダ式を返すプロパティ、定義時に初期化子を与えたものも試してみることにしました。
IL を読んでみる
ビルドして生成された IL を、 ReSharper の IL Viewer で読んでみることにします。
が、結構長いので、一つずつ分けて読んでいくことにします。
自動実装プロパティ( Name )
.field private string '< Name>k__BackingField' .custom instance void [System.Runtime]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = (01 00 00 00 ) .custom instance void [System.Diagnostics.Debug]System.Diagnostics.DebuggerBrowsableAttribute::.ctor(valuetype [System.Diagnostics.Debug]System.Diagnostics.DebuggerBrowsableState) = (01 00 00 00 00 00 00 00 ) // ........ // int32(0) // 0x00000000 .method public hidebysig specialname instance string get_Name() cil managed { .custom instance void [System.Runtime]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = (01 00 00 00 ) .maxstack 8 // [7 30 - 7 34] IL_0000: ldarg.0 // this IL_0001: ldfld string ConsoleApp1.SampleClass::'< Name>k__BackingField' IL_0006: ret } // end of method SampleClass::get_Name .method public hidebysig specialname instance void set_Name( string 'value' ) cil managed { .custom instance void [System.Runtime]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = (01 00 00 00 ) .maxstack 8 // [7 35 - 7 39] IL_0000: ldarg.0 // this IL_0001: ldarg.1 // 'value' IL_0002: stfld string ConsoleApp1.SampleClass::'< Name>k__BackingField' IL_0007: ret } // end of method SampleClass::set_Name .method public hidebysig specialname rtspecialname instance void .ctor() cil managed { .maxstack 8 // [9 9 - 9 29] IL_0000: ldarg.0 // this IL_0001: call instance void [System.Runtime]System.Object::.ctor() IL_0006: nop // [10 9 - 10 10] IL_0007: nop // [11 13 - 11 28] IL_0008: ldarg.0 // this IL_0009: ldstr "hello" IL_000e: call instance void ConsoleApp1.SampleClass::set_Name(string) IL_0013: nop // [12 9 - 12 10] IL_0014: ret } // end of method SampleClass::.ctor .method public hidebysig instance void CallSample() cil managed { .maxstack 8 // [15 9 - 15 10] IL_0000: nop // [16 13 - 16 45] IL_0001: ldstr "N1 " IL_0006: ldarg.0 // this IL_0007: call instance string ConsoleApp1.SampleClass::get_Name() IL_000c: call string [System.Runtime]System.String::Concat(string, string) IL_0011: call void [System.Console]System.Console::WriteLine(string) IL_0016: nop // [18 9 - 18 10] IL_0017: ret } // end of method SampleClass::CallSample .property instance string Name() { .get instance string ConsoleApp1.SampleClass::get_Name() .set instance void ConsoleApp1.SampleClass::set_Name(string) } // end of property SampleClass::Name
上から見ていくと、 < Name>k__BackingField という private 変数、 get_Name/set_Name という名前で Getter/Setter メソッドが生成されていることがわかります。
またその下のコンストラクタ、 CallSample() を見ると、set_Name() 、 get_Name() が呼ばれています。
変数のようにふるまう、とは言ったものの、実際にはほかのクラスからアクセスした場合同様、メソッドとして実行されているようです。
_name 、 GetName() 、 SetName()
.field private string _name .method public hidebysig instance string GetName() cil managed { .maxstack 1 .locals init ( [0] string V_0 ) // [10 9 - 10 10] IL_0000: nop // [11 13 - 11 26] IL_0001: ldarg.0 // this IL_0002: ldfld string ConsoleApp1.SampleClass::_name IL_0007: stloc.0 // V_0 IL_0008: br.s IL_000a // [12 9 - 12 10] IL_000a: ldloc.0 // V_0 IL_000b: ret } // end of method SampleClass::GetName .method public hidebysig instance void SetName( string 'value' ) cil managed { .maxstack 8 // [14 9 - 14 10] IL_0000: nop // [15 13 - 15 27] IL_0001: ldarg.0 // this IL_0002: ldarg.1 // 'value' IL_0003: stfld string ConsoleApp1.SampleClass::_name // [16 9 - 16 10] IL_0008: ret } // end of method SampleClass::SetName .method public hidebysig specialname rtspecialname instance void .ctor() cil managed { .maxstack 8 // [7 9 - 7 40] IL_0000: ldarg.0 // this IL_0001: ldstr "world" IL_0006: stfld string ConsoleApp1.SampleClass::_name // [18 9 - 18 29] IL_000b: ldarg.0 // this IL_000c: call instance void [System.Runtime]System.Object::.ctor() IL_0011: nop // [19 9 - 19 10] IL_0012: nop // [20 13 - 20 29] IL_0013: ldarg.0 // this IL_0014: ldstr "hello" IL_0019: stfld string ConsoleApp1.SampleClass::_name // [21 13 - 21 30] IL_001e: ldarg.0 // this IL_001f: ldstr "hello" IL_0024: call instance void ConsoleApp1.SampleClass::SetName(string) IL_0029: nop // [21 38 - 21 39] IL_002a: ret } // end of method SampleClass::.ctor .method public hidebysig instance void CallSample() cil managed { .maxstack 8 // [24 9 - 24 10] IL_0000: nop // [25 13 - 25 46] IL_0001: ldstr "N2 " IL_0006: ldarg.0 // this IL_0007: ldfld string ConsoleApp1.SampleClass::_name IL_000c: call string [System.Runtime]System.String::Concat(string, string) IL_0011: call void [System.Console]System.Console::WriteLine(string) IL_0016: nop // [27 13 - 27 50] IL_0017: ldstr "N3 " IL_001c: ldarg.0 // this IL_001d: call instance string ConsoleApp1.SampleClass::GetName() IL_0022: call string [System.Runtime]System.String::Concat(string, string) IL_0027: call void [System.Console]System.Console::WriteLine(string) IL_002c: nop // [29 9 - 29 10] IL_002d: ret } // end of method SampleClass::CallSample
手動で書いている、ということや変数・メソッドの名前以外、自動実装プロパティとほとんど一緒なのかな?と思っていましたが、結構違っていますね。
例えば自動実装プロパティには System.Runtime.CompilerServices.CompilerGeneratedAttribute が自動生成された変数の下に登場しますが、こちらでは登場しません。
また Getter/Setter メソッドを見ても、手動実装の GetName() では自動実装プロパティの get_Name() にないローカル変数へのストア( stloc.0 )などが存在しします。
今一つ分かっていないのですが、このローカル変数というのは、戻り値なのでしょうか。
.method public hidebysig specialname instance string get_Name() cil managed { .custom instance void [System.Runtime]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = (01 00 00 00 ) .maxstack 8 // [7 30 - 7 34] IL_0000: ldarg.0 // this IL_0001: ldfld string ConsoleApp1.SampleClass::'< Name>k__BackingField' IL_0006: ret } // end of method SampleClass::get_Name .method public hidebysig instance string GetName() cil managed { .maxstack 1 .locals init ( [0] string V_0 ) // [10 9 - 10 10] IL_0000: nop // [11 13 - 11 26] IL_0001: ldarg.0 // this IL_0002: ldfld string ConsoleApp1.SampleClass::_name IL_0007: stloc.0 // V_0 IL_0008: br.s IL_000a // [12 9 - 12 10] IL_000a: ldloc.0 // V_0 IL_000b: ret } // end of method SampleClass::GetName
Get-Only プロパティ( GetOnlyName )
.field private initonly string '< GetOnlyName>k__BackingField' .custom instance void [System.Runtime]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = (01 00 00 00 ) .custom instance void [System.Diagnostics.Debug]System.Diagnostics.DebuggerBrowsableAttribute::.ctor(valuetype [System.Diagnostics.Debug]System.Diagnostics.DebuggerBrowsableState) = (01 00 00 00 00 00 00 00 ) // ........ // int32(0) // 0x00000000 .method public hidebysig specialname instance string get_GetOnlyName() cil managed { .custom instance void [System.Runtime]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = (01 00 00 00 ) .maxstack 8 // [7 37 - 7 41] IL_0000: ldarg.0 // this IL_0001: ldfld string ConsoleApp1.SampleClass::'< GetOnlyName>k__BackingField' IL_0006: ret } // end of method SampleClass::get_GetOnlyName .method public hidebysig specialname rtspecialname instance void .ctor() cil managed { .maxstack 8 // [9 9 - 9 29] IL_0000: ldarg.0 // this IL_0001: call instance void [System.Runtime]System.Object::.ctor() IL_0006: nop // [10 9 - 10 10] IL_0007: nop // [11 13 - 11 35] IL_0008: ldarg.0 // this IL_0009: ldstr "hello" IL_000e: stfld string ConsoleApp1.SampleClass::'< GetOnlyName>k__BackingField' // [12 9 - 12 10] IL_0013: ret } // end of method SampleClass::.ctor .method public hidebysig instance void CallSample() cil managed { .maxstack 8 // [15 9 - 15 10] IL_0000: nop // [16 13 - 16 52] IL_0001: ldstr "N4 " IL_0006: ldarg.0 // this IL_0007: call instance string ConsoleApp1.SampleClass::get_GetOnlyName() IL_000c: call string [System.Runtime]System.String::Concat(string, string) IL_0011: call void [System.Console]System.Console::WriteLine(string) IL_0016: nop // [18 9 - 18 10] IL_0017: ret } // end of method SampleClass::CallSample .property instance string GetOnlyName() { .get instance string ConsoleApp1.SampleClass::get_GetOnlyName() } // end of property SampleClass::GetOnlyName
正直 Setter メソッドがないだけで、ほとんど同じかな?と思っていたのですが、一つ面白い違いがありますね。
コンストラクタで値を代入しているときに、 Setter メソッド(的なもの)が呼ばれるのではなく、生成された変数( < GetOnlyName>k__BackingField )に対して値が代入されています。
// [11 13 - 11 35] IL_0008: ldarg.0 // this IL_0009: ldstr "hello" IL_000e: stfld string ConsoleApp1.SampleClass::'< GetOnlyName>k__BackingField'
ラムダ式を返す( GetName2 、 GetName3 、 GetName4 )
~省略~ .method public hidebysig specialname instance string get_GetName2() cil managed { .maxstack 8 // [10 35 - 10 40] IL_0000: ldarg.0 // this IL_0001: ldfld string ConsoleApp1.SampleClass::_name IL_0006: ret } // end of method SampleClass::get_GetName2 .method public hidebysig specialname instance string get_GetName3() cil managed { .maxstack 8 // [11 35 - 11 39] IL_0000: ldarg.0 // this IL_0001: call instance string ConsoleApp1.SampleClass::get_Name() IL_0006: ret } // end of method SampleClass::get_GetName3 .method public hidebysig specialname instance string get_GetName4() cil managed { .maxstack 8 // [12 35 - 12 42] IL_0000: ldstr "world" IL_0005: ret } // end of method SampleClass::get_GetName4 ~省略~ .method public hidebysig instance void CallSample() cil managed { .maxstack 2 // [21 9 - 21 10] IL_0000: nop // [22 13 - 22 49] IL_0001: ldstr "N5 " IL_0006: ldarg.0 // this IL_0007: call instance string ConsoleApp1.SampleClass::get_GetName2() IL_000c: call string [System.Runtime]System.String::Concat(string, string) IL_0011: call void [System.Console]System.Console::WriteLine(string) IL_0016: nop // [24 13 - 24 49] IL_0017: ldstr "N6 " IL_001c: ldarg.0 // this IL_001d: call instance string ConsoleApp1.SampleClass::get_GetName3() IL_0022: call string [System.Runtime]System.String::Concat(string, string) IL_0027: call void [System.Console]System.Console::WriteLine(string) IL_002c: nop // [26 13 - 26 49] IL_002d: ldstr "N7 " IL_0032: ldarg.0 // this IL_0033: call instance string ConsoleApp1.SampleClass::get_GetName4() IL_0038: call string [System.Runtime]System.String::Concat(string, string) IL_003d: call void [System.Console]System.Console::WriteLine(string) IL_0042: nop // [28 9 - 28 10] IL_0043: ret } // end of method SampleClass::CallSample ~省略~ .property instance string GetName2() { .get instance string ConsoleApp1.SampleClass::get_GetName2() } // end of property SampleClass::GetName2 .property instance string GetName3() { .get instance string ConsoleApp1.SampleClass::get_GetName3() } // end of property SampleClass::GetName3 .property instance string GetName4() { .get instance string ConsoleApp1.SampleClass::get_GetName4() } // end of property SampleClass::GetName4
先ほどと重複する部分(自動実装プロパティなど)は省略しています。
ラムダ式を使った場合も、メソッドを呼び出す Get-Only プロパティが生成される、という結果になっています。
自動実装プロパティに初期化子を渡す( Name5 )
.field private string '< Name5>k__BackingField' .custom instance void [System.Runtime]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = (01 00 00 00 ) .custom instance void [System.Diagnostics.Debug]System.Diagnostics.DebuggerBrowsableAttribute::.ctor(valuetype [System.Diagnostics.Debug]System.Diagnostics.DebuggerBrowsableState) = (01 00 00 00 00 00 00 00 ) // ........ // int32(0) // 0x00000000 .method public hidebysig specialname instance string get_Name5() cil managed { .custom instance void [System.Runtime]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = (01 00 00 00 ) .maxstack 8 // [7 31 - 7 35] IL_0000: ldarg.0 // this IL_0001: ldfld string ConsoleApp1.SampleClass::'< Name5>k__BackingField' IL_0006: ret } // end of method SampleClass::get_Name5 .method public hidebysig specialname instance void set_Name5( string 'value' ) cil managed { .custom instance void [System.Runtime]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = (01 00 00 00 ) .maxstack 8 // [7 36 - 7 40] IL_0000: ldarg.0 // this IL_0001: ldarg.1 // 'value' IL_0002: stfld string ConsoleApp1.SampleClass::'< Name5>k__BackingField' IL_0007: ret } // end of method SampleClass::set_Name5 .method public hidebysig instance void CallSample() cil managed { .maxstack 8 // [10 9 - 10 10] IL_0000: nop // [11 13 - 11 46] IL_0001: ldstr "N8 " IL_0006: ldarg.0 // this IL_0007: call instance string ConsoleApp1.SampleClass::get_Name5() IL_000c: call string [System.Runtime]System.String::Concat(string, string) IL_0011: call void [System.Console]System.Console::WriteLine(string) IL_0016: nop // [13 9 - 13 10] IL_0017: ret } // end of method SampleClass::CallSample .method public hidebysig specialname rtspecialname instance void .ctor() cil managed { .maxstack 8 // [7 45 - 7 58] IL_0000: ldarg.0 // this IL_0001: ldstr "hello world" IL_0006: stfld string ConsoleApp1.SampleClass::'< Name5>k__BackingField' IL_000b: ldarg.0 // this IL_000c: call instance void [System.Runtime]System.Object::.ctor() IL_0011: nop IL_0012: ret } // end of method SampleClass::.ctor .property instance string Name5() { .get instance string ConsoleApp1.SampleClass::get_Name5() .set instance void ConsoleApp1.SampleClass::set_Name5(string) } // end of property SampleClass::Name5
自動実装プロパティに初期化子を入れた場合も、 Setter メソッドが呼ばれるのではなく、生成された変数に対して値が代入されています。
おわりに
今回見た違いがパフォーマンスなどで影響してくるか?というと、あまりないような気はします。
ただ、書き方として同じように見えても、 IL を比べてみるとあれこれ違う、というのは面白いですね。
(雑な感想)
参照
【C#】string interpolation で format string に変数を使いたかった
はじめに
この記事は C# Advent Calendar 2018 の六日目の穴埋め記事です。
以前 string interpolation を使って遊んでみたことがありました が、使っている中で困った話を。
何をしたかったか
// 「CurrentTime: 2018-12-10 21:12:23 616」 のように出力される. Console.WriteLine($"CurrentTime: {DateTime.Now:yyyy-MM-dd HH:mm:ss fff}");
上記のようなコードで、 yyyy-MM-dd HH:mm:ss fff の部分を変数として定義し、他のコードと共有したいと思いました。
で、実際にやってみました。
string dateFormat = "yyyy-MM-dd HH:mm:ss fff"; Console.WriteLine($"CurrentTime: {DateTime.Now:dateFormat}");
出力された結果は
CurrentTime: 10a午e7or12a午
……誰だいチミは???
IL Viewer などで見たところ、 dateFormat の中身( yyyy-MM-dd HH:mm:ss fff )ではなく、「dateFormat」という文字列を format string として認識し、フォーマットをかけているようでした。
評価のタイミングが違うからおかしな文字列になるわけですね。
/(^o^)\
解決
$"CurrentTime: {DateTime.Now:{{dateFormat}}}" などあれこれ試したもののうまく解決できず。
下記のように書くのが良さそう、という結論になりました。
string dateFormat = "yyyy-MM-dd HH:mm:ss fff"; Console.WriteLine($"CurrentTime: {DateTime.Now.ToString(dateFormat)}");
う~ん、なんかしっくりこない気がするようなしないような。
とにかく問題は解決できたので良しとします。
コードを読んでみる
さて、せっかくなので、もう少し中身を見てみることにします。
ReSharper の IL Viewer を使って今回想定通り動作してくれたコードを見てみます。
まずは元のコードから。
Console.WriteLine($"CurrentTime: {DateTime.Now:yyyy-MM-dd HH:mm:ss fff}"); Console.WriteLine($"CurrentTime: {DateTime.Now.ToString(dateFormat)}");
では IL を見てみましょう。
// [17 13 - 17 87] IL_0001: ldstr "CurrentTime: {0:yyyy-MM-dd HH:mm:ss fff}" IL_0006: call valuetype [System.Runtime]System.DateTime [System.Runtime]System.DateTime::get_Now() IL_000b: box [System.Runtime]System.DateTime IL_0010: call string [System.Runtime]System.String::Format(string, object) IL_0015: call void [System.Console]System.Console::WriteLine(string) IL_001a: nop // [21 13 - 21 84] IL_003b: ldstr "CurrentTime: " IL_0040: call valuetype [System.Runtime]System.DateTime [System.Runtime]System.DateTime::get_Now() IL_0045: stloc.1 // V_1 IL_0046: ldloca.s V_1 IL_0048: ldloc.0 // dateFormat IL_0049: call instance string [System.Runtime]System.DateTime::ToString(string) IL_004e: call string [System.Runtime]System.String::Concat(string, string) IL_0053: call void [System.Console]System.Console::WriteLine(string) IL_0058: nop
上は String.Format を、下は DateTime.ToString を使っています。
は良いのですが、上の方はボックス化されていますね。
string.Format の引数は string, object であるため、値型の DateTime を渡すとボックス化が発生する、と。
これを見ると、しっくりこないとか言っていた $"{DateTime.Now.ToString(dateFormat)}" の方が良い、という結果になりそうです。
やっぱりどのように動作するかはちゃんと確認しないとだめですね(てへぺろ)。
なお、string.Format を使って上と同じ内容を実現するコードはこちら。
Console.WriteLine($"CurrentTime: {string.Format("{0:yyyy-MM-dd HH:mm:ss fff}", DateTime.Now)}");
当然といえば当然ですが、こちらもボックス化が発生するため、 DateTime を扱う上では素直に ToString を使うのが良さそうです。
ついでに string.Format と DateTime.ToString のソースコードの URL も貼っておきます。
string.Format
string クラスは partial クラスとして処理が分割されているため、探すのにちょっと苦労しました(;゚Д゚)
しっかりとは読めていませんが、引数として渡された formatString を一文字ずつ解析し、 IFormattable を継承している DateTime クラスの ToString() を実行しているようです。
DateTime.ToString
こちらもちょろっと読んだだけですが、引数として渡された formatString を一文字ずつ解析してフォーマット、という流れは変わらないようです。
参照
【C#】Queue で遊ぶ
はじめに
この記事は C# その2 Advent Calendar 2018 七日目の記事です。
前回 と同じく、dotnet cli のコードを辿っていく中で見かけた、 Queue クラスについてです。
Queue クラスについて
下記のような特徴があります。
- Enqueue() で要素を追加し、Dequeue() または Peek() で追加した要素を追加した順に取り出す(先入れ先出し)。
- インデックスを指定して任意の順番の要素をとることはできない。
- Dequeue() で要素を取り出した場合は該当の要素が Queue の中から取り除かれ、 Peek() の場合はそのまま。
- Queue の中身が空の状態で Dequeue() や Peek() を実行すると InvaridOperationException 。
- TryDequeue(out T) や TryPeek(out T) を使うと、エラーを発生させずに値が取り出せる。
- IEnumerable を継承しているので foreach を使って取り出すこともできる。
- foreach や Linq を使って要素を取り出した場合、 Peek() と同じく Queue の中身は減らない。
- 非ジェネリック版も存在する。が、通常使うことはない。
- スレッドセーフではない( ConcurrentQueue を使う)
また前回の ReadOnlyCollection のようにコンストラクタで IEnumerable を渡し、その内容・順番で Queue を作ることができます。
ただし、その中身は複製されたもので、参照は別物となります。 (IReadOnlyCollection などのように元の値を変更しても影響を受けない)
SampleValue.cs
public class SampleValue { public string Name { get; set; } }
Program.cs
List< SampleValue> values = new List< SampleValue> { new SampleValue { Name = "Hello", }, new SampleValue { Name = "World", }, }; Queue< SampleValue> queueFromList = new Queue< SampleValue>(values); foreach (var q in queueFromList) { Console.WriteLine(q.Name); } Console.WriteLine("change1"); values[0] = new SampleValue { Name = "Good" }; foreach (var q in queueFromList) { Console.WriteLine(q.Name); // Hello World とそれぞれ出力される. } Console.WriteLine("change2"); values = values.OrderByDescending(v => v.Name).ToList(); foreach (var q in queueFromList) { Console.WriteLine(q.Name); // Hello World とそれぞれ出力される. }
List の用途限定版、といった趣がありますが、どのように利用できるかを List と比較しながら見ていきたいと思います。
どう使うか
- 要素を追加した順番に取り出したいとき。
。。。いや、そりゃそうなんですけれども。
参考に、 dotnet cli (正確には CliCommandLineParser )でどのように使われているかを見てみます。
CliCommandLineParser > Microsoft.DotNet.Cli.CommandLine > Parser.cs
internal ParseResult Parse( IReadOnlyCollection< string> rawArgs, bool isProgressive) { var unparsedTokens = new Queue< Token>( NormalizeRootCommand(rawArgs) .Lex(configuration)); var rootAppliedOptions = new AppliedOptionSet(); var allAppliedOptions = new List< AppliedOption>(); var errors = new List< OptionError>(); var unmatchedTokens = new List< string>(); while (unparsedTokens.Any()) { var token = unparsedTokens.Dequeue(); if (token.Type == TokenType.EndOfArguments) { // stop parsing further tokens break; } if (token.Type != TokenType.Argument) { var definedOption = DefinedOptions.SingleOrDefault(o => o.HasAlias(token.Value)); if (definedOption != null) { var appliedOption = allAppliedOptions .LastOrDefault(o => o.HasAlias(token.Value)); if (appliedOption == null) { appliedOption = new AppliedOption( definedOption, token.Value); rootAppliedOptions.Add(appliedOption); } allAppliedOptions.Add(appliedOption); continue; } } var added = false; foreach (var appliedOption in Enumerable.Reverse(allAppliedOptions)) { var option = appliedOption.TryTakeToken(token); if (option != null) { allAppliedOptions.Add(option); added = true; break; } if (token.Type == TokenType.Argument && appliedOption.Option.IsCommand) { break; } } if (!added) { unmatchedTokens.Add(token.Value); } } if (rootAppliedOptions.Command()?.TreatUnmatchedTokensAsErrors == true) { errors.AddRange( unmatchedTokens.Select(UnrecognizedArg)); } if (configuration.RootCommandIsImplicit) { rawArgs = rawArgs.Skip(1).ToArray(); var appliedOptions = rootAppliedOptions .SelectMany(o => o.AppliedOptions) .ToArray(); rootAppliedOptions = new AppliedOptionSet(appliedOptions); } return new ParseResult( rawArgs, rootAppliedOptions, isProgressive, configuration, unparsedTokens.Select(t => t.Value).ToArray(), unmatchedTokens, errors); }
コマンドライン引数として渡された文字列を、引数の種類(コマンド、オプションなど)と値の組み合わせにまとめたもの( Token )の Queue を作り、それを順に解析しているようです。
Queue を使っているのは、インデックス指定で要素を取り出したり取り除いたり、 という処理を避けているためかと思います。
確かに、先入れ先出しがしたい場合、 List を使って手動で 0 番目の要素を取って、 RemoveAt(0) を実行するより、間違いも少なく分かりやすいと思います。
パフォーマンス比較
ちょっと mono に寄り道することになりますが、 Unity の Profiler を使って、 Queue と List を比較してみます。
using System.Collections.Generic; using UnityEngine; using UnityEngine.Profiling; using UnityEngine.UI; public class MainController : MonoBehaviour { public Button QueueButton; public Button ListButton; void Start () { QueueButton.onClick.AddListener(() => { Profiler.BeginSample("QueueSample"); RepeatQueue(10000000); Profiler.EndSample(); }); ListButton.onClick.AddListener(() => { Profiler.BeginSample("QueueSample"); RepeatList(10000000); Profiler.EndSample(); }); } private void RepeatQueue(int count) { Queue< string> textQueue = new Queue< string>(); for (int i = 0; i < count; i++) { textQueue.Enqueue("hello"); string message = textQueue.Dequeue(); } } private void RepeatList(int count) { List< string> texts = new List< string>(); for (int i = 0; i < count; i++) { texts.Add("hello"); string message = texts[0]; texts.RemoveAt(0); } } }
各ボタンを2回ずつ押し、 Profiler の値を比較しました。
GC Alloc や速度を見ると、 List の方が良さそうですね。
RemoveAt() を別で実行している分遅いのかな、と思っていました。
要素を取り除かない場合
Queue の Peek() を使い、 List は RemoveAt() を消して、要素を取り除かない場合も比較してみました。
private void RepeatQueue(int count) { Queue< string> textQueue = new Queue< string>(); for (int i = 0; i < count; i++) { textQueue.Enqueue("hello"); string message = textQueue.Peek(); } } private void RepeatList(int count) { List< string> texts = new List< string>(); for (int i = 0; i < count; i++) { texts.Add("hello"); string message = texts[0]; } }
今度は Queue の方が良好な結果になっています。
ただ、実際のユースケースにおいてこれらの違いがどの程度効いてくるかというと。。。
よっぽどボトルネックになっている場合を除き、あまり気にはしなくても良いかもしれませんね。
コードを見てみる
そもそも、 Queue や List で要素を追加したり、取り出したりするとき、内部的にはどのようなことが行われているのでしょうか。
.NET Core ではこれらのクラスは .NET Core Libraries (CoreFX) として提供されています。
Queue.cs
using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; namespace System.Collections.Generic { ~省略~ public class Queue< T> : IEnumerable< T>, System.Collections.ICollection, IReadOnlyCollection< T> { private T[] _array; private int _head; // The index from which to dequeue if the queue isn't empty. private int _tail; // The index at which to enqueue if the queue isn't full. private int _size; // Number of elements. private int _version; [NonSerialized] private object _syncRoot; private const int MinimumGrow = 4; private const int GrowFactor = 200; // double each time ~省略~ public void Enqueue(T item) { if (_size == _array.Length) { int newcapacity = (int)((long)_array.Length * (long)GrowFactor / 100); if (newcapacity < _array.Length + MinimumGrow) { newcapacity = _array.Length + MinimumGrow; } SetCapacity(newcapacity); } _array[_tail] = item; MoveNext(ref _tail); _size++; _version++; } ~省略~ public T Dequeue() { int head = _head; T[] array = _array; if (_size == 0) { ThrowForEmptyQueue(); } T removed = array[head]; if (RuntimeHelpers.IsReferenceOrContainsReferences< T>()) { array[head] = default; } MoveNext(ref _head); _size--; _version++; return removed; } ~省略~ private void SetCapacity(int capacity) { T[] newarray = new T[capacity]; if (_size > 0) { if (_head < _tail) { Array.Copy(_array, _head, newarray, 0, _size); } else { Array.Copy(_array, _head, newarray, 0, _array.Length - _head); Array.Copy(_array, 0, newarray, _array.Length - _head, _tail); } } _array = newarray; _head = 0; _tail = (_size == capacity) ? 0 : _size; _version++; } ~省略~
Enqueue() 、 Dequeue() と、それに関連しそうなコードを抜粋してみました。
※2018/12/27 更新
Enqueue() の時の要素数の増やし方に間違いがあったため修正しました。
× 要素数を 4 増やす -> 〇 現在の 2 倍( 2 倍にした後の要素数が 4 未満の場合は 4 )に増やす
t さん、ご指摘ありがとうございます(..)_
- 内部的には配列で値を持っている。
- 要素数( Count で返す値)は配列の要素数をそのまま返すのではなく、 _size として保持している。
- Enqueue() の時に、配列の要素数が _size と同じ場合、配列の要素数を現在の 2 倍( 2 倍にした後の要素数が 4 未満の場合は 4 )に増やす。
- Dequeue() では取り出した値を削除(デフォルト値に戻す)するが、配列の要素数自体は変更しない。
- Dequeue() で取り出す対象のインデックスは _head として保持している。
List.cs
using System.Collections.ObjectModel; using System.Diagnostics; using System.Runtime.CompilerServices; using System.Threading; namespace System.Collections.Generic { ~省略~ public class List< T> : IList< T>, IList, IReadOnlyList< T> { private const int DefaultCapacity = 4; private T[] _items; // Do not rename (binary serialization) private int _size; // Do not rename (binary serialization) private int _version; // Do not rename (binary serialization) [NonSerialized] private object _syncRoot; private static readonly T[] s_emptyArray = new T[0]; ~省略~ [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Add(T item) { _version++; T[] array = _items; int size = _size; if ((uint)size < (uint)array.Length) { _size = size + 1; array[size] = item; } else { AddWithResize(item); } } // Non-inline from List.Add to improve its code quality as uncommon path [MethodImpl(MethodImplOptions.NoInlining)] private void AddWithResize(T item) { int size = _size; EnsureCapacity(size + 1); _size = size + 1; _items[size] = item; } ~省略~ private void EnsureCapacity(int min) { if (_items.Length < min) { int newCapacity = _items.Length == 0 ? DefaultCapacity : _items.Length * 2; // Allow the list to grow to maximum possible capacity (~2G elements) before encountering overflow. // Note that this check works even when _items.Length overflowed thanks to the (uint) cast if ((uint)newCapacity > Array.MaxArrayLength) newCapacity = Array.MaxArrayLength; if (newCapacity < min) newCapacity = min; Capacity = newCapacity; } } ~省略~ public void RemoveAt(int index) { if ((uint)index >= (uint)_size) { ThrowHelper.ThrowArgumentOutOfRange_IndexException(); } _size--; if (index < _size) { Array.Copy(_items, index + 1, _items, index, _size - index); } if (RuntimeHelpers.IsReferenceOrContainsReferences< T>()) { _items[_size] = default; } _version++; } ~省略~ } }
- 内部的には配列で値を持っている。
- 要素数( Count で返す値)は配列の要素数をそのまま返すのではなく、 _size として保持している。
- Add() 実行時、 _size が配列の要素数と同じかそれ以上なら、配列の要素数 0 の場合は 4 に、それ以外は現在の二倍に配列の要素数を増やす。
- RemoveAt() では削除したインデックスが、 デクリメントした _size 未満なら Array.Copy で取り除く。そうでなければ該当インデックスの要素にデフォルト値を入れる。
見た感じ List の方が処理が重そうな気がするのですが、要素追加時に内部の配列の要素数の増やし方で違いが出ているのかもしれませんね。
おわりに
Queue クラスは、要素を先入れ先出しでのみ取り出すことが分かっていて、かつ値を保持するタイミングと使用するタイミングが離れている場合(そうでないなら IEnumerable とかで良さそう)に一番活躍してくれるのでは、と思いました。
長々と書き連ねた意味は(私が楽しかったという以外に)なかったかもしれませんが。
ConcurrentQueue も近い内触ってみようと思います。多分。
参照
【C#】readonly と ReadOnlyCollection
はじめに
この記事は C# その2 Advent Calendar 2018 三日目の記事です。
前回書いた通り、最近は dotnet cli などのコードをたどっています。
その中で IReadOnlyCollection などを使って処理の結果などを返しているのが目につきました。
これまで自分ではあまり使ったことがなかったのですが、気になったので調べてみることにした、というお話です。
readonly と IReadOnlyCollection
まずは readonly キーワードと IReadOnlyCollection の違いについて調べてみます。
private readonly List< string> _texts1 = new List< string>(); private IReadOnlyCollection< string> _texts2 = new List< string>();
readonly は List のインスタンス自身が ReadOnly になるため、コンストラクタなどで初期化した後、 null や new List< string>() などを入れようとするとエラーが発生します。
IReadOnlyCollection は List (配列) 内の要素が ReadOnly となり、 Add や Clear 、インデックスによる要素アクセスといった要素を変更するメソッドが提供されません。
要するに get-only プロパティと同じようなもの、という感じでしょうか。
また、上記の初期化の様子からもわかるかもしれませんが、 List や配列は IReadOnlyCollection を継承しているため、 IReadOnlyCollection としてふるまうことができます。
注意点としては下記が挙げられます。
- readonly キーワードと違い、 List のインスタンス自身は書き換えてしまえること
- ①List を作る → ② ①を元に IReadOnlyCollection を作る とした場合、①を変更すると ②も変更されてしまうこと
List< string> originalValues = new List< string> { "Hello", }; IReadOnlyCollection< string> readonlyValues = originalValues; foreach (string v in readonlyValues) { Console.WriteLine(v); // Hello と出力される. } originalValues[0] = "World"; foreach (string v in readonlyValues) { Console.WriteLine(v); // World と出力される. }
ここから、元の List (配列) から IReadOnlyCollection を作る場合、というのは、あくまでメソッドの引数などに使用し、このメソッドでは要素を変更しませんよ、という形で使うのが良さそうです。
あと、当然ながら IReadOnlyCollection にも readonly キーワードは使用できるため、注意点 1. に対応することはできます。
private readonly IReadOnlyCollection< string> _texts = new List< string>();
慣れてないと???と思ってしまいそうですが。
IReadOnlyCollection と ReadOnlyCollection
名前から言えば、 ReadOnlyCollection は IReadOnlyCollection に対する具象クラス( concrete class )?という気がします。
実際のところ違いは下記のようなものが挙げられます。
- List や配列は継承していない
( new ReadOnlyCollection(List や配列のインスタンス) で生成する) - Contains や CopyTo 、 IndexOf など IReadOnlyCollection が持たないメソッドを持つ
- List や配列など、 IList を継承しているクラスのインスタンスからのみ生成できる
( ReadOnlyCollection は IReadOnlyCollection からは生成できない)
3.から、戻り値は List ではなく IEnumerable で使う、といったように、一般的には具象クラスでなく抽象クラスや interface で返しましょう、という話をよく聞きます。
が、 ReadOnlyCollection と IReadOnlyCollection に関しては、例えば ReadOnlyCollection のメソッドが必要な場合などはちゃんと ReadOnlyCollection として返す必要がありそうです。
その他の注意点としては下記が挙げられます。
- readonly キーワードと違い、 List のインスタンス自身は書き換えてしまえること
- ①List を作る → ② ①を元に IReadOnlyCollection を作る とした場合、①を変更すると ②も変更されてしまうこと
IReadOnlyCollection と一緒やん!(; ・`д・´)
ということで気を付けて使いましょう。
各要素のパブリック変数
もう一つ注意点として、 readonly 、 IReadOnlyCollection 、 ReadOnlyCollection ともに各要素が持つパブリック変数については ReadOnly にならない、ということが挙げられます。
SampleValue.cs
namespace ConsoleApp1 { public class SampleValue { public string Name { get; set; } } }
Program.cs
using System; using System.Collections.Generic; using System.Collections.ObjectModel; namespace ConsoleApp1 { class Program { static void Main(string[] args) { ReadOnlyCollection readonlyValues = new ReadOnlyCollection< SampleValue> ( new List< SampleValue> { new SampleValue { Name = "Hello", } }); foreach (var v in readonlyValues) { Console.WriteLine(v.Name); // Hello と表示される. } foreach (var v in readonlyValues) { v.Name = "World"; } foreach (var v in readonlyValues) { Console.WriteLine(v.Name); // World と表示される. } } } }
これを防ぎたい場合は SampleValue 内の変数を get-only プロパティにするなどの対策が必要です。
おわりに
ReadOnly という字面だけ見て判断してしまうと、躓いてしまいそうなことがわかりました。
とはいえちゃんと活用すれば、IReadOnlyCollection や ReadOnlyCollection はとても便利だと思うので、必要に応じて積極的に使用していきたいと思います。
参照
【.NET Core】dotnet newの話 その1
はじめに
このブログは .NET, .NET Core, monoのランタイム・フレームワーク・ライブラリ Advent Calendar 2018 二日目の記事です。
普段気軽に実行している dotnet new コマンドですが、どんなことが行われているのかが気になったので調べてみました。
環境
- Windows 10 ver.1803 build 17134.407
- .NET Core 2.1.403
コマンドを実行してログを見てみる
何はともあれコマンドを実行してみます。
dotnet new console -n ConsoleSample
実行の結果作成されたファイルは下記のとおりです。
生成されたファイル
- ConsoleSample.csproj
- Program.cs
- obj
- ConsoleSample.csproj.nuget.cache
- ConsoleSample.csproj.nuget.g.props
- ConsoleSample.csproj.nuget.g.targets
- project.assets.json
プロジェクトファイル( ConsoleSample.csproj )とメインクラス( Program.cs )、残りは NuGet に関連するファイルのようです。
出力されたログの内容を見てみます。
テンプレート "Console Application" が正常に作成されました。 作成後のアクションを処理しています... 'dotnet restore' を ConsoleSample\ConsoleSample.csproj で実行しています... C:\Users\XXX\OneDrive\Documents\workspace\ConsoleSample\ConsoleSample.csproj のパッケージを復元しています... MSBuild ファイル C:\Users\XXX\OneDrive\Documents\workspace\ConsoleSample\obj\ConsoleSample.csproj.nuget.g.props を生成しています。 MSBuild ファイル C:\Users\XXX\OneDrive\Documents\workspace\ConsoleSample\obj\ConsoleSample.csproj.nuget.g.targets を生成しています。 C:\Users\XXX\OneDrive\Documents\workspace\ConsoleSample\ConsoleSample.csproj の復元が 627.22 ms で完了しました。 正常に復元されました。
ログからわかるのは下記でしょうか。
- dotnet new の実行完了後に自動で dotnet restore が呼ばれている
- 下記のファイルは dotnet new で生成され、それを元に dotnet restore が行われているらしい
- ConsoleSample.csproj
- Program.cs
- obj/project.assets.json
- dotnet restore では MSBuild ファイルが生成されている
dotnet new コマンドを探す
new に限らず、 dotnet コマンド( dotnet.exe )の実態は CLI(Command Line Interface) であるようです。
(余談ですが、 .NET で CLI というと、 Common Language Infrastructure などと混じってややこしいですね)
ということで、ここから何が行われているかを辿ってみたいと思います。
Program.cs
CLI のメインクラスは dotnet/Progarm.cs です。
ここで何をしているかというと。。。
- "DOTNET_CLI_CAPTURE_TIMING" という値が環境変数に設定されているかを確認します。
例えば 1 が設定されていれば Thread1 のパフォーマンストレースが出力されるようになります。 - ターミナルのデフォルトの文字コードを取得して、その文字コードでログ出力するよう設定
- "info" や "help" などの引数が渡されているかを確認し、あれば該当の内容を出力
- Telemetryの設定
- dotnet/BuiltinCommandsCatalog.cs で、入力されたコマンドが組み込みで用意されているかを確認
- dotnet/BuiltInCommandMetadata.cs として、commands/dotnet-new/NewCommandShim.cs を渡す
- dotnet/Parser.cs の Instance.ParseFrom で、渡された引数が問題なくパースできるかを確認
- コマンドの実行
となるようです。
これ以降も同様ですが、多分だいぶ端折っているところがあると思いますので、詳しくはコードをたどってみてください。
なお 5.で組み込みコマンドではなかった場合、 CommandFactory/CommandFactoryUsingResolver.cs でコマンドが生成されるようですが、今回は追いかけていません。
Telemetry について
Program.cs を含め、随所に登場するのが Telemetry 関連のコードです。
dotnet new などのコマンドを実行したときの OS や .NET などのバージョン情報などを匿名で収集している、ということのようです。
このデータは Microsoft や .NET Team だけでなく、私たちも利用することができるようです。
今回はやりませんが、このデータを使って何かできないかを考えてみるのも面白そうです。
なおこれを止めるには、環境変数 "DOTNET_CLI_TELEMETRY_OPTOUT" を値 1 または true で登録すると良いようです。
処理としては dotnet/Telemetry/Telemetry.cs でクライアントの登録やトラッキングするイベントの登録などを行い、 Microsoft.DotNet.Cli.Utils/TelemetryEventEntry.cs の EntryPosted で通知しているようです。
NewCommandShim.cs
ようやく dotnet new コマンドの中身に触れられるぞ!と思うも、実際にはほぼ Microsoft.TemplateEngine.Cli/New3Command.cs を呼んでいるだけです。
- SessionID を取得する
- Telemetry のログ設定
- Microsoft.TemplateEngine.Cli/New3Command.cs に情報を渡して実行 (New3Command.Run)
Microsoft.TemplateEngine.Cli/New3Command.cs のソースコードは下記で見ることができます。
https://github.com/dotnet/templating
New3Command.Run
3.の引数では、言語 (C#) やフレームワークのバージョン、テンプレートからプロジェクトを生成する Microsoft.TemplateEngine.Orchestrator.RunnableProjects/RunnableProjectGenerator.cs の Assembly 情報などを Host として渡しています。
他にも、 templating からコールバックで呼ばれ、(ファイルが存在する場合は) Microsoft.TemplateEngine.Edge.dll がある C:\Program Files\dotnet\sdk\2.1.403 の Templates にある .nupkg ファイルの一覧を取得し、 インストールする FirstRun などを渡しています。
インストールは Microsoft.TemplateEngine.Cli/Installer.cs で実行しており、この部分は次回追いかけてみたいと思います。
なお C:\Program Files\dotnet\sdk\2.1.403\Templates にある .nupkg ファイルは下記のとおりです。
- microsoft.dotnet.common.itemtemplates.1.0.2-beta3.nupkg
- microsoft.dotnet.common.projecttemplates.2.1.1.0.2-beta3.nupkg
- microsoft.dotnet.test.projecttemplates.2.1.1.0.2-beta3-20180716-1864993.nupkg
- microsoft.dotnet.web.itemtemplates.2.1.5.nupkg
- microsoft.dotnet.web.projecttemplates.2.1.2.1.5.nupkg
- microsoft.dotnet.web.spa.projecttemplates.2.1.5.nupkg
- nunit3.dotnetnew.template.1.5.1.nupkg
一番関係がありそうなファイルは nunit3.dotnetnew.template 辺りでしょうか。
Visual studio Code の NuGet NuPkg Viewer で見ると、下記のような内容でした。
Metadata: id: NUnit3.DotNetNew.Template version: 1.5.1 title: NUnit 3 template for dotnet-new authors: akharlov owners: akharlov requireLicenseAcceptance: 'false' licenseUrl: 'https://github.com/nunit/dotnet-new-nunit/blob/master/LICENSE' projectUrl: 'https://github.com/nunit/dotnet-new-nunit' iconUrl: 'https://avatars2.githubusercontent.com/u/2678858' description: Project and item templates containing basic NUnit 3 Test Project. releaseNotes: "- update nunit dependency to v3.10.1\r\n- update NUnit3TestAdapter dependency to v3.10.0\r\n- update Microsoft.NET.Test.Sdk dependency to v15.7.2\r\n- add new `--framework` supported parameters:\r\n + .NET Framework versions (net472)\r\n- add test fixture item templates" copyright: Copyright © Aleksei Kharlov aka halex2005 language: en-US tags: NUnit dotnet core packageTypes: '[object Object]' Contents: - File: _rels/.rels - File: [Content_Types].xml - File: Content/dotnet-new-nunit-csharp-test-item/.template.config/template.json - File: Content/dotnet-new-nunit-csharp-test-item/UnitTest1.cs - File: Content/dotnet-new-nunit-csharp/.template.config/dotnetcli.host.json - File: Content/dotnet-new-nunit-csharp/.template.config/template.json - File: Content/dotnet-new-nunit-csharp/Company.TestProject1.csproj - File: Content/dotnet-new-nunit-csharp/UnitTest1.cs - File: Content/dotnet-new-nunit-fsharp-test-item/.template.config/template.json - File: Content/dotnet-new-nunit-fsharp-test-item/UnitTest1.fs - File: Content/dotnet-new-nunit-fsharp/.template.config/dotnetcli.host.json - File: Content/dotnet-new-nunit-fsharp/.template.config/template.json - File: Content/dotnet-new-nunit-fsharp/Company.TestProject1.fsproj - File: Content/dotnet-new-nunit-fsharp/Program.fs - File: Content/dotnet-new-nunit-fsharp/UnitTest1.fs - File: Content/dotnet-new-nunit-visualbasic-test-item/.template.config/template.json - File: Content/dotnet-new-nunit-visualbasic-test-item/UnitTest1.vb - File: Content/dotnet-new-nunit-visualbasic/.template.config/dotnetcli.host.json - File: Content/dotnet-new-nunit-visualbasic/.template.config/template.json - File: Content/dotnet-new-nunit-visualbasic/Company.TestProject1.vbproj - File: Content/dotnet-new-nunit-visualbasic/UnitTest1.vb - File: NUnit3.DotNetNew.Template.nuspec - File: package/services/metadata/core-properties/cccaa6ca700c4f9193a9511aac24f9d9.psmdcp
これがどう関係してくるのか、といったところは次回追いかけてみたいと思います。
終わりに
普段何気なく使っているツールも、処理を一つ一つ追ってみると面白いですね。
ただどこかにメモっていかないとすぐこんがらがってしまうので、この記事を書いてみることにしました。
次回は dotnet/templating を追いかけてみたいと思います。
参照
【ASP.NET Core】Razorで遊んでみる
はじめに
ブログとしては久しぶりに ASP.NET Core 関連のお話です。
C# のコードを HTML に書けるということで ASP.NET Core に採用されている Razor 。
実をいうとあんまり使われておらず、 Angular や React などの JavaScript のフレームワークが主流なんじゃないの? と思っていました。
が、とある勉強会にて C# 版 WebAssembly である Blazor の話が盛り上がったのですが、その構文がかなり Razor と共通していると感じました。
で、あれ? もしかして結構本気でこの構文を推し進めようとしているのでは?
(もちろんジョークだと思っていたわけではないですが)
と思ったのでした。
Blazor にも興味があるところですが、まずは情報もよりたくさんあるであろう Razor から調べてみることにしました。
というお話です。
なお Microsoft Docs をはじめ、 Razor 構文は紹介されているところがたくさんあるため、網羅的に試すというよりは気になったものをピックアップして取り上げることにします。
試してみる
では早速試してみることにします。
基本的に C# で書くことのできるコードは、頭に@をつけることで何でも書くことができるようです。
また @ が複数行にわたる場合など、{} や () でまとめることも可能です。
@{ var text = "Hello "; var sample = ViewBag.Sample; } < div> @(text + sample) < /div>
{} や () の使用は必須ではないものがほとんどですが、List< string> のように <> が含まれる場合は必ず {} や () 内に入れる必要があります。
@functions
関数は @functions の中に書くことができます。
@functions { string GetMessage() { return "Hello"; } } < div>@GetMessage()< /div>
@classes のようなものは用意されていないため、インナークラスを定義することはできないようです...と思いきや、 @functions は実は関数が書ける、というものではなくコードブロックが書けるものであるため、下記のように書くことができてしまいます。
@functions { public class InnerClassSample { public string Name { get; set; } } async Task< InnerClassSample> GetInnerClassAsync() { await Task.Delay(500); return new InnerClassSample{ Name = "Hello" }; } }
ただし当然ながら? コントローラークラスなどからはアクセスできないため、活躍の場は少なそうです。
Controller から値を渡す
Controller 側から値を渡す方法としては ViewData、ViewBagがあります。
いずれも Controller で値を入れておき、 View 側で取り出す、という形をとります。
HomeController.cs
using System; using System.Linq; using Microsoft.AspNetCore.Mvc; using RazorSample.Models; namespace RazorSample.Controllers { [Route("")] public class HomeController: Controller { public IActionResult Index() { // ViewData で渡す値をセット. ViewData["SampleUsers"] = Enumerable.Range(0, 5) .Select(num => new User { ID = num, Name = "User " + num, Description = "Hello World!!!" }) .ToList(); return View("/Views/Index.cshtml"); } } }
Index.cshtml
@foreach (User u in (List< User>)ViewData["SampleUsers"]) { < table> < tr> < td>ID: < /td> < td>@u.ID< /td> < /tr> < tr> < td>Name: < /td> < td>@u.Name< /td> < /tr> < tr> < td>Description: < /td> < td>@u.Description< /td> < /tr> < /table> }
ViewData で受け取れる値の型は object のため、キャストしてから取り出します。
ViewData と ViewBag の違いはアクセスの方法です。
ViewData[Sample] = "Hello"; ViewBag.Sample = "Hello";
注意点として、 ViewBag の Sample もどこかで定義されているわけではないため、 入力補完や静的解析は効かない、ということです。
ということで使い分けは気分次第、ということになります。 ( ViewBag は NullReferenceException がとか読んだ気もしますが、 Dictionary も Key が見つからなきゃエラーになりますよね)
コメントアウト
@{ /* DoSomething(); */ // DoSomething(); }
↑でもコメントアウトできることはできます。
が、
@* @DoSomething() *@
がスマートですね。
動的に呼び出せるか
できません。
ページをロードするときに評価され、 HTML に変換されてしまうので、
< button type="button" onclick=@{ Say(); }>Say< /button>
のように書くと、ページロード時に Say() が実行され、 onclick は空の状態になります(ボタンを押しても反応しない)。
加えて
< !--button type="button" onclick=@{ Say(); }>< /button-->
としてもコメントアウトとみなされないため、注意が必要そうです。
async/await
@functions { string message = "default"; async Task< string> GenerateMessage() { await Task.Delay(1000); return "hello"; } } < div> @{ message = await GenerateMessage(); } @message @* これでもOK *@ @await GenerateMessage() < /div>
上記のように async/await を使うこと自体はできますが、非同期で表示の内容を切り替えられるわけではないため、 ページのロード完了は await などで待つ必要があります。
非同期での実行がしたい場合は Blazor?
@:
@: を頭につけると、その行は < text>< /text> で囲まれたのと同じ扱いとなります。
@for (var i = 0; i < 3; i++) { var person = "person " + i; @:Name: @person この行の @: 以降はすべて HTML として出力される }
HtmlTagHelper
Razor では直接 HTML タグを書く他に、より C# 的に書けるようヘルパーが用意されています。
Index.cshtml
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers @model RazorSample.Models.User @* using を使うことでスコープの終わりに EndForm を自動で実行してくれる BeginForm の引数は 1. アクション( Submit で呼び出す関数)、 2. コントローラー名、 3. 送信方法( Get 、 Post)、 4. HTML の属性( class とか) *@ @using(Html.BeginForm( "SendMessage", "Home", FormMethod.Post, new { @class = "myclass"})){ < table> < tr> < td>@Html.LabelFor(m => m.Name)< /td> < td>@Html.TextBoxFor(m => m.Name)< /td> < /tr> < tr> < td>@Html.LabelFor(m => m.Description)< /td> < td>@Html.TextAreaFor(m => m.Description)< /td> < /tr> < /table> < input type="submit" value="Submit" /> }
User.cs
using System; namespace RazorSample.Models { public class User { public int ID { get; set; } public string Name { get; set; } public string Description { get; set; } public DateTime LastUpdateDate { get; set; } } }
HomeController.cs
using System; using System.Linq; using Microsoft.AspNetCore.Mvc; using RazorSample.Models; namespace RazorSample.Controllers { [Route("")] public class HomeController: Controller { ~省略~ [HttpPost] public void SendMessage(User sampleUser) { Console.WriteLine("Send"); } } }
Formなどに対してusing を使えるのは便利そうですね。
JavaScript での C# の変数
例えばこんな処理
Index.cshtml
@functions { string message = "default message"; async Task< string> GetMessageAsync() { await Task.Delay(500); return "hello"; } } < script type="text/javascript"> var clickSample = function() { // '' や "" で囲まないとエラー console.log('@message'); } < /script> < div> @{ message = await GetMessageAsync(); } < /div> < button onclick="clickSample()">Click< /button>
まず地味にハマったのは、 JavaScript の console.log で C# の変数を渡すときに '' や "" で囲んでおらず、「 default 」が定義されていない、とエラーになったことです。
「 default 」ってなんぞ?と思ったら、変数の「 default message 」が半角スペースで区切られたものを変数として扱おうとしていたためのようでした。
やはり動的に型を持つ言語は扱いが違うので慣れていないとびっくりしますね(;'∀')
あと、 JavaScript 内での C# の変数の値は JavaScript のコードがロードされた時に確定されるため、上記のように「 @{ message = await GetMessageAsync(); } 」より前に定義すると値は「default message」に、後に定義すると「 hello 」になります。
async/await などを使う場合は注意が必要ですね。
おわりに
取り留めもなく書いていきましたが、かなり C# 的に HTML が書ける、というのはなかなか面白いです。
とはいえ非同期の処理は使えなかったり(実行はされるが表示に反映されない)、どのように HTML に変換されるかを理解する必要がある、という意味ではやはり HTML をちゃんとわかっておく必要があったりします。
今回取り上げた内容もまだ Razor の一部でしかないと思うので、引き続き追いかけてみるとともに Blazor も試してみたいと思います。
参照
- ASP.NET Core の Razor 構文リファレンス - Microsoft Docs
- Introduction to Razor Pages in ASP.NET Core - Microsoft Docs
- Programming ASP.NET Core
- Tag Helpers in ASP.NET Core - Microsoft Docs
- ASP.Net MVC Html.BeginForm Tutorial with example - ASPSnippets
- asp.net - Razor Syntax in External Javascript - Stack Overflow
【Unity】画面サイズと異なる大きさの RenderTexture でクリック位置の WorldPoint が取りたい話
はじめに
Unity では基本的に、 uGUI などの GUI はカメラを使って表示する 3D より手前に表示されます。
ただ、例えばポップアップウインドウのような表示がしたいなど、 GUI より手前に 3D を表示したい場合もあります。
それを解決する方法として RenderTexture を使う方法があります(正しい方法かどうかは不明)。
が、それを使って表示した内容に対してクリックし、その位置を 3D(WorldPoint) に変換しようとしたらうまくいかなかったのでメモです。
準備
- 画面サイズは 1920 * 1200 とします。
- RenderTexture のサイズは 1100 * 600 とします。
- Canvas を下記の内容で準備します。
- Canvas Scaler の UI Scale Mode を Scale with Screen Size にし、Reference Resolution を 1920 * 1200 とします。
- Raw Image を追加し、サイズを 1100 * 600 に設定します。
- 2.の Anchors を X = 0, Y = 1 に設定します。
- RenderTexture のサイズを 1100 * 600 で作成し、2.の Texture にアタッチします。
- 今回は特定の位置に置いた Plane 上のクリック位置を取ります。
- カメラの正面に Plane を適当なサイズに置きます(カメラから Z 方向に 10 離す)。
- クリック位置が分かりやすいように、適当なサイズの Cube を置きます。
(初期位置はどこでも良いです)
クリック位置を取ってみる
using System.Collections; using System.Collections.Generic; using UnityEngine; public class MainController : MonoBehaviour { public Camera MainCamera; // RenderTexture public RectTransform TextureTransform; // クリック位置上に置く Cube. public Transform ClickTransform; // Plane public Transform PlaneTransform; private Vector2 texturePosition; private Vector2 screenScale; private Vector3 screenInputPosition = Vector3.zero; private void Start () { // 実行中の画面サイズを 1920 * 1200 にスケーリング. screenScale = new Vector2( Screen.width / 1920f, Screen.height / 1200f); // 今回は RenderTexture の位置が動的に変わらないものとする. texturePosition = TextureTransform.position; } private void Update () { if(Input.GetMouseButtonUp(0)) { // クリック位置が RenderTexture でのどの位置にあるかを算出. // 座標値は画面サイズ 1920 * 1200 での値を使う必要がある. screenInputPosition.x = (Input.mousePosition.x - texturePosition.x) / screenScale.x; screenInputPosition.y = ((Input.mousePosition.y - texturePosition.y) / screenScale.y) + TextureTransform.sizeDelta.y; // クリック位置を取りたい Plane と カメラの Z 軸の差分を入れることで、 Plane 上の座標値に変換できる. screenInputPosition.z = PlaneTransform.localPosition.z - MainCamera.transform.localPosition.z; ClickTransform.position = MainCamera.ScreenToWorldPoint(screenInputPosition); } } }
注意点としては、 2D の Y 座標は画面上部が 0 、3D は下が 0 になる、というところと、 ScreenToWorldPoint に渡す座標は画面サイズが 1920 * 1200 であったときのものに合わせる必要がある、ということです。