以前 TryRoslyn と言われてたサービスですが、今は Sharplab という名になっています。
このサービスを使うと、コードがILやネイティブコードにどのようにコンパイルされるか確認したり、実行したりオブジェクトのメモリ状態を確認できます。
例えば次の図は、構造体の文字列がどのようなメモリ状態なのかを示したものです。
Sharplab を使って、コードだけでなくメモリ状態を可視化することで理解を深めるきっかけにできるか見てみましょう。
目次
TL;DR;
Shaplab を使って可視化することで「分からないことを、何が分からないかわかるようにできる」と思うので活用するといいと思います。
LinqPadも似てますが、併用するとより幸せになれます。
Sharplabの基本
Sharplab にアクセスしてみましょう。
次のようなシンプルなクラスが表示されていると思います。
using System; public class C { public void M() { } }
ここに色々コードを書いて試すことができます。
いつ使うのか
スマホやPCでも入力に困らないレベルでインテリセンスによるコード入力補助があります。さらに後述するコードの実行、解析、C#バージョンの選択までできます。
加えて、入力したコードが自動的に一意なurl で再表示できるので、コードの共有にもいいでしょう。
だいたいここまでできるとLinqPad でできることが網羅されており、nuget を使わない限りはLinqPadより気軽で便利な面も多いでしょう。
基本的な文法、Decompileや言語バージョンの違いの確認程度なら、Sharplab でいい感じがします。
コードの共有
何かしらコードを入力するとURLがついて、入力ごとに変化しているのが分かると思います。
このURLを踏むとそのコードを表示できるのでコードで状態を共有出来て非常に便利です。実際、C# のGitHub Issue などで SharpLabのURLで再現コードをシェアしているのも見かけます。
この記事でもコードごとにURLをシェアしてみましょう。
言語選択
画面上部のCodeから C# 以外に VB.NET や F# が選べます。
表示の切り替え(Decompile)
下(あるいは右)ペインResults を選ぶといくつか選択肢があります。Decompileのまとまりから見ていきましょう。
Decompile の中で C# を選ぶとデコンパイルしたC#コード、IL を選ぶとIL状態、JIT Asm を選ぶとネイティブコードを確認できます。
言語ごとのデコンパイル結果の比較
余談ですが、C#、VB.NET、F# それぞれで同じようなコードを書いた時のデコンパイル結果を最も簡単に確認できるサービスの1つかもしれません。
C# の次のコードを使ってデコンパイル結果を見てみましょう。
using System; public class C { public void M() { } }
using System.Diagnostics; using System.Reflection; using System.Runtime.CompilerServices; using System.Security; using System.Security.Permissions; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.Default | DebuggableAttribute.DebuggingModes.DisableOptimizations | DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints | DebuggableAttribute.DebuggingModes.EnableEditAndContinue)] [assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)] [assembly: AssemblyVersion("0.0.0.0")] [module: UnverifiableCode] public class C { public void M() { } }
VB.NET を選んで表示される C# Decompile 結果は C# とほぼ同じですね。
Imports System Public Class C Public Sub M() End Sub End Class
using System.Diagnostics; using System.Reflection; using System.Runtime.CompilerServices; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.Default | DebuggableAttribute.DebuggingModes.DisableOptimizations | DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints | DebuggableAttribute.DebuggingModes.EnableEditAndContinue)] [assembly: AssemblyVersion("0.0.0.0")] public class C { public void M() { } }
F# だとガラッと雰囲気違うのは興味深いです。
open System type C() = member this.M() = ()
using Microsoft.FSharp.Core; using System; using System.Reflection; [assembly: FSharpInterfaceDataVersion(2, 0, 0)] [assembly: AssemblyVersion("0.0.0.0")] [CompilationMapping(SourceConstructFlags.Module)] public static class _ { [Serializable] [CompilationMapping(SourceConstructFlags.ObjectType)] public class C { public C() { ((object)this)..ctor(); } public void M() { } } } namespace <StartupCode$_> { internal static class $_ { } }
Other
表示切替の中に、Decompile以外にOther があります。
Syntax もその1つで、Sharplab を使うことでRoslyn によるSyntaxTree の状態も確認できます。
Roslyn拡張を書くときなどににらめっこしたりすることがありますが、LinqPad以外でもきれいにみれるのはうれしいものがあります。
Verify Only を選ぶことでコンパイルできるかの確認ができます。そもそもコード入力中にがしがし評価されてエラーがずらずらでるので、Verifyしなくとも気付けるのですが。
Explain を選ぶと、余り使われないシンタックスの説明が表示されます。例えば、予約語を変数名に使う @var
シンタックスを使ってみましょう。
using System; public class C { public void M() { var @var = ""; } }
@var is a verbatim identifier. [Docs]
Prefix @ allows keywords to be used as a name (for a variable, parameter, field, class, etc).
For example, M(string string) doesn't compile, while M(string @string) does.
インターフェースのデフォルト実装など、新しい機能はまだ説明がないのでちまちまIssueを見るといいです。
interface Hoge { string Text {get;set;} public void Fuga() { Console.WriteLine(Text); } }
Run
コードを実行できるので、LinqPad で軽く実行するのをWebで試せると思うとC# の書き捨てコードとしては相当楽なのではないでしょうか。
この時、結果表示にはConsole.WriteLine()
によるstdout 以外にも、<何かしらのオブジェクト>.Inspect()
拡張メソッドでも表示できます。Inspectメソッドは、LinqPadの.Dump()
に近い感覚でグラフィカルにオブジェクトが表示されるのでいい感じです。
他にもヒープ状態の確認に使えるInspect.Heap()
メソッドや、スタック状態の確認に使えるInspect.Stack()
メソッドがあります。
using System; // Run mode: // value.Inspect() — often better than Console.WriteLine // Inspect.Heap(object) — structure of an object in memory (heap) // Inspect.Stack(value) — structure of a stack value public static class Program { public static void Main() { Console.WriteLine("🌄"); new [] {"hoge", "moge"}.Inspect(); Inspect.Heap(new [] {"pi", "yo"}); Inspect.Stack(new [] {1, 2}); } }
C# のメモリ状態を確認する
さて、この記事の本筋に戻りましょう。C# を書いているとコードからは挙動やILを見ないと判断に迷うケースは少なからずあります。私は少なくともたくさんあります。
Visual StudioやVS Codeでパッと思いつくいい感じのやり方がなく、私はもっぱらLinqPad でコードを実行しつつILを見たりして確認していました。UnityのProfilerを使うという手もありますが、少しノイズが多くやりにくさはぬぐえません。
メモリの状態を確認するには、メモリの状態が可視化されるのが直感的だと思うのですが、それがない/そこまでが遠いのですね。
そんな時にSharplab の`Inspect.MemoryGraph()
メソッドを使うとメモリマップが可視化され、わかりやすさを大いに助けてくれます。早速これを使ってBoxing (ボックス化) と、構造体におけるstring の参照のされ方、クラスではどう変わるのかを見てみましょう。
Boxing を可視化する
Boxing は、C# で起こりやすい割に案外分かりにくいと感じる状態の1つです。説明は適当に資料に譲るとして、int をボックス化してobjectに代入した状態を考えてみましょう。
using System; public static class Program { public static void Main() { var x = 5; object y = x; Inspect.MemoryGraph(y); } }
明らかに普段書かないコードのように見えますが、場合によっては書きます。ここまでわかりやすいとパット頭でBoxing状態が書けそうですが、実際に思い描いたものと一緒か見るのに 変数y
を Inspect.MemoryGraph(o)
として可視化してみましょう。
では、int配列をobject配列でボックス化するとどうなるでしょうか。
using System; public static class Program { public static void Main() { var o = new object[] {1, 2, 3}; Inspect.MemoryGraph(o); } }
変数o
を Inspect.MemoryGraph(o)
として可視化してみましょう。
戻り値がvoidだとInspectしようがないので可視化できないので、次のコードはエラーになります。
using System; public static class Program { public static void Main() { Inspect.MemoryGraph(Console.WriteLine(1)); } }
「intがobjectでボックス化されている」と言葉で聞いて、コードでも書けるときはいいのですが、「これもしかしてボックス化してる?」という時にサクッと書いて、メモリの状態を可視化できるのはかなり便利だと思います。
構造体における文字列の参照状態を確認する
こんな疑問があります。
string をフィールドに持つ構造体のインスタンスを引数に入れたりするとき、string は文字列全体丸っとコピーされるのか参照 (先頭アドレス) だけコピーされるのかどっちだっけ?
— じんぐる (@xin9le) February 12, 2019
(こんなことで悩んでるあたりダメダメ過ぎる感あるけど、実際わかんない)
ILを見ればすぐにわかるケースですがSharplabを使えば可視化されます。次のコードを模擬コードとしてみてみましょう。値型のint も持たせて参照型のstringとどう違うかついでに確認します。
public static class Program { public static void Main() { var s = new S(); s.text = "before"; s.num = 1; var t = s.text; var h = s; h.num = 2; s.text = "after"; Inspect.MemoryGraph(s, t, h); } struct S { public string text; public int num; } }
構造体は、参照型であるstring(文字列) を参照で持っていることが ref
からわかりますね。
変数s は、after
という文字列を代入しているので別の文字列を参照していることが分かります。
ちなみにLinqPad で結果を見ると次のようになります。
IL 見れば、なるほどldloca
、Ldfld
。
IL_0000: nop IL_0001: ldloca.s 00 // s IL_0003: initobj UserQuery.S IL_0009: ldloca.s 00 // s IL_000B: ldstr "before" IL_0010: stfld UserQuery+S.text IL_0015: ldloca.s 00 // s IL_0017: ldc.i4.1 IL_0018: stfld UserQuery+S.num IL_001D: ldloc.0 // s IL_001E: ldfld UserQuery+S.text IL_0023: stloc.1 // t IL_0024: ldloc.0 // s IL_0025: stloc.2 // h IL_0026: ldloca.s 02 // h IL_0028: ldc.i4.2 IL_0029: stfld UserQuery+S.num IL_002E: ldloca.s 00 // s IL_0030: ldstr "after" IL_0035: stfld UserQuery+S.text IL_003A: ldloc.0 // s IL_003B: call LINQPad.Extensions.Dump<S> IL_0040: pop IL_0041: ldloc.1 // t IL_0042: call LINQPad.Extensions.Dump<String> IL_0047: pop IL_0048: ldloc.2 // h IL_0049: call LINQPad.Extensions.Dump<S> IL_004E: pop IL_004F: ret
クラスにおける参照状態を確認する
先ほどの構造体Sを、クラスにするとどうなるか見てみましょう。
public static class Program { public static void Main() { var s = new S(); s.text = "before"; s.num = 1; var t = s.text; var h = s; h.num = 2; s.text = "after"; Inspect.MemoryGraph(s, t, h); } class S { public string text; public int num; } }
構造体と違い、それぞれの値が参照でつながっているのが分かりますが、分かりにくいですね!LinqPadで実行すると、クラスの時と構造体の時で結果が変わっていることが分かります。
違いは次の通りです。
オブジェクト | 構造体 | クラス |
---|---|---|
s.text | after | after |
s.num | 1 | 2 |
t | before | before |
h.text | before | after |
h.num | 2 | 2 |
コードとメモリの状態を見ると理由は明らかです。
ShapLab で楽しいC# コーディングを過ごしましょう!