以前TryRoslynと言われてたサービスですが、今はSharplabという名になっています。
このサービスを使うと、コードがILやネイティブコードにどのようにコンパイルされるか確認したり、実行したりオブジェクトのメモリ状態を確認できます。
例えば次の図は、構造体の文字列がどのようなメモリ状態なのかを示したものです。
Sharplabを使って、コードだけでなくメモリ状態を可視化することで理解を深めるきっかけにできるか見てみましょう。
概要
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# 以外にVisual Basic.NETやF# が選べます。
表示の切り替え(Decompile)
下(あるいは右)ペインResultsを選ぶといくつか選択肢があります。Decompileのまとまりから見ていきましょう。
Decompileの中でC# を選ぶとデコンパイルしたC#コード、ILを選ぶとIL状態、JIT Asmを選ぶとネイティブコードを確認できます。
言語ごとのデコンパイル結果の比較
余談ですが、C#、Visual Basic.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() { } }
Visual Basic.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); } }
https://github.com/ashmind/language-syntax-explanations/issues
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に代入した状態を考えてみましょう。
https://docs.microsoft.com/ja-jp/dotnet/csharp/programming-guide/types/boxing-and-unboxing
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でボックス化されている」と言葉で聞いて、コードでも書けるときはいいのですが、「これもしかしてボックス化してる?」という時にサクッと書いて、メモリの状態を可視化できるのはかなり便利です。
構造体における文字列の参照状態を確認する
こんな疑問があります。
じんぐる (@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# コーディングを過ごしましょう!