20200805のC#に関する記事は5件です。

[C# 9.0 Preview] デリゲートをFunction Pointerに置き換えたら速くなるのか実験した

免責事項

  • 現時点ではPreviewの機能です。
  • Visual Studio Version 16.7.0 Preview 6.0(.NET Core 3.1.400-preview-015203)時点での結果です。
  • 効果は当然ですがソフトウェアに依ります。
  • 置き換え方が間違っていたらごめんなさい。

イントロ

C# 9.0から、unsafeコンテキスト限定ですがFunction Pointersが使用できるようになります。
(まだ多少構文が変わるそうですが。ところで日本語ではそのまま「関数ポインター」でいいのだろうか?)

従来からあるデリゲートは高機能ですが、オーバーヘッドが多少あります。[^要出典]
実際のソフトウェアで、Function Pointersに置き換えることで高速化されるか実験してみました。

対象ソフトウェア

最近私がPull Requestを送っていた Cysharp/ZStringというライブラリでは、値を文字列にする処理を、型毎にデリゲートで保持しています。
そのため、(JITで消え去りそうですけど)少なからずデリゲート呼び出しのコストがかかっているはずです。
Function Pointersにするにはうってつけ?のハズです。

先に結果だけ書くと、数パーセント速くなってそうでした。

計測

Nugetの最新版(2.2.0)と、それをベースにFunction Pointersに置き換えたコードをBenchmarkDotNetで比較します。
コードとベンチマークコードはGithubに一応置いておきますが、書き捨てるつもりなのでしばらくしたら消すかもしれません。
この記事の最後にpatchとして置いておきます。

ベンチマーク結果のMethodで、末尾に_が付いているのがリリース版(デリゲート版)、Nが付いているのが置き換えたコード(Function Pointer版)です。

なお、ZString にはUtf16用のクラスとUtf8用のクラスがありますが、面倒なのでUtf16用しか置き換えと計測をしていません。

ベンチマークコードはFormatBenchmark.csdotnet run -c Releaseで実行しています。

結果(1回目)

Ratio列に注目してください。
多少速くなっていそうな雰囲気があります。
どうしても計測時環境によりブレがありますので、もう一回測ってみます。

Method FormatString Mean Error StdDev Ratio RatioSD Code Size Gen 0 Gen 1 Gen 2 Allocated
Format_ This (...)orld. [62] 225.71 ns 1.708 ns 1.598 ns 1.00 0.00 3147 B 0.0181 - - 152 B
FormatN This (...)orld. [62] 192.91 ns 2.135 ns 1.997 ns 0.85 0.01 3458 B 0.0181 - - 152 B
Utf16PreparedFormat_ This (...)orld. [62] 85.97 ns 1.762 ns 2.097 ns 1.00 0.00 2597 B 0.0181 - - 152 B
Utf16PreparedFormatN This (...)orld. [62] 84.32 ns 1.686 ns 1.731 ns 0.98 0.02 2567 B 0.0181 - - 152 B
Utf16StringBuilderAppendFormat_ This (...)orld. [62] 201.26 ns 0.881 ns 0.824 ns 1.00 0.00 7425 B - - - -
Utf16StringBuilderAppendFormatN This (...)orld. [62] 177.24 ns 0.850 ns 0.795 ns 0.88 0.01 7924 B - - - -
Format_ x:{0}, y:{1} 137.85 ns 0.466 ns 0.436 ns 1.00 0.00 3147 B 0.0057 - - 48 B
FormatN x:{0}, y:{1} 135.39 ns 0.747 ns 0.699 ns 0.98 0.01 3458 B 0.0057 - - 48 B
Utf16PreparedFormat_ x:{0}, y:{1} 74.40 ns 0.694 ns 0.649 ns 1.00 0.00 2597 B 0.0057 - - 48 B
Utf16PreparedFormatN x:{0}, y:{1} 74.23 ns 0.627 ns 0.586 ns 1.00 0.01 2567 B 0.0057 - - 48 B
Utf16StringBuilderAppendFormat_ x:{0}, y:{1} 141.08 ns 0.671 ns 0.628 ns 1.00 0.00 7423 B - - - -
Utf16StringBuilderAppendFormatN x:{0}, y:{1} 131.62 ns 0.580 ns 0.543 ns 0.93 0.01 7922 B - - - -
BenchmarkDotNet=v0.12.1, OS=Windows 10.0.19042
AMD Ryzen 7 3700X, 1 CPU, 16 logical and 8 physical cores
.NET Core SDK=3.1.400-preview-015203
  [Host]     : .NET Core 3.1.6 (CoreCLR 4.700.20.26901, CoreFX 4.700.20.31603), X64 RyuJIT
  DefaultJob : .NET Core 3.1.6 (CoreCLR 4.700.20.26901, CoreFX 4.700.20.31603), X64 RyuJIT

結果(2回目)

遅くなっているケースもありますが、傾向として速くなってそうです。

Method FormatString Mean Error StdDev Ratio RatioSD Code Size Gen 0 Gen 1 Gen 2 Allocated
Format_ This (...)orld. [62] 225.79 ns 1.438 ns 1.345 ns 1.00 0.00 3147 B 0.0181 - - 152 B
FormatN This (...)orld. [62] 192.97 ns 1.582 ns 1.321 ns 0.85 0.01 3458 B 0.0181 - - 152 B
Utf16PreparedFormat_ This (...)orld. [62] 85.96 ns 1.667 ns 1.784 ns 1.00 0.00 2597 B 0.0181 - - 152 B
Utf16PreparedFormatN This (...)orld. [62] 90.81 ns 1.874 ns 1.924 ns 1.06 0.02 2567 B 0.0181 - - 152 B
Utf16StringBuilderAppendFormat_ This (...)orld. [62] 191.97 ns 0.974 ns 0.911 ns 1.00 0.00 7276 B - - - -
Utf16StringBuilderAppendFormatN This (...)orld. [62] 176.76 ns 0.877 ns 0.821 ns 0.92 0.01 7924 B - - - -
Format_ x:{0}, y:{1} 137.57 ns 0.816 ns 0.763 ns 1.00 0.00 3147 B 0.0057 - - 48 B
FormatN x:{0}, y:{1} 137.22 ns 0.797 ns 0.746 ns 1.00 0.01 3458 B 0.0057 - - 48 B
Utf16PreparedFormat_ x:{0}, y:{1} 73.85 ns 0.731 ns 0.683 ns 1.00 0.00 2597 B 0.0057 - - 48 B
Utf16PreparedFormatN x:{0}, y:{1} 76.60 ns 0.685 ns 0.641 ns 1.04 0.01 2567 B 0.0057 - - 48 B
Utf16StringBuilderAppendFormat_ x:{0}, y:{1} 140.42 ns 0.659 ns 0.584 ns 1.00 0.00 7425 B - - - -
Utf16StringBuilderAppendFormatN x:{0}, y:{1} 125.47 ns 0.545 ns 0.510 ns 0.89 0.00 7922 B - - - -

結論

  • 対象は数パーセントは効果あり。
  • しかし、関数ポインターできるのは静的関数のみで、設計にかなり制約が出てくる。 (例えば、外からデリゲートを渡しづらくなる)

これよりもっとデリゲートを使っているとか、複雑さを犠牲にしてでも1%でも速くしたいとかで無ければ、置き換えるのは最後の手段にしておきましょう。

コードとベンチマークコード

追記:実はコードは少し前(Preview 4.0か5.0)に作っていて、その時はFunction Pointerとvoid*との相互変換ができませんでした。(ドキュメントでは許可されている)
そのため、ちょっと回りくどいコードになっています。
しかし、Preview 6.0ではvoid*との相互変換できるようになっていたため、今ならもう少しオリジナルのコードに近いものにできそうです。


0001-function-pointers.patch
From bab609ea44ed09d24537c279de043d8072615f5d Mon Sep 17 00:00:00 2001
From: udaken <u*********@gmail.com>
Date: Wed, 5 Aug 2020 23:12:23 +0900
Subject: [PATCH] function pointers

---
 global.json                                   |   6 +
 .../BenchmarkVsReleasedVersion.csproj         |   2 +-
 .../BuiltinTypesBenchmark.cs                  |  12 +-
 .../FormatBenchmark.cs                        |  12 +-
 sandbox/BenchmarkVsReleasedVersion/Program.cs |   3 +-
 .../Assets/Scripts/ZString/PreparedFormat.cs  |  64 +++---
 .../Utf16ValueStringBuilder.AppendFormat.cs   |   2 +-
 ...Utf16ValueStringBuilder.CreateFormatter.cs | 217 ++++++++++--------
 .../ZString/Utf16ValueStringBuilder.cs        |  77 ++++---
 src/ZString/PreparedFormat.cs                 |  64 +++---
 src/ZString/PreparedFormat.tt                 |   2 +-
 .../Utf16ValueStringBuilder.AppendFormat.cs   |   2 +-
 .../Utf16ValueStringBuilder.AppendFormat.tt   |   2 +-
 ...Utf16ValueStringBuilder.CreateFormatter.cs | 217 ++++++++++--------
 ...Utf16ValueStringBuilder.CreateFormatter.tt |  42 ++--
 src/ZString/Utf16ValueStringBuilder.cs        |  77 ++++---
 src/ZString/ZString.csproj                    |   2 +
 17 files changed, 437 insertions(+), 366 deletions(-)
 create mode 100644 global.json

diff --git a/global.json b/global.json
new file mode 100644
index 0000000..5679eb1
--- /dev/null
+++ b/global.json
@@ -0,0 +1,6 @@
+{
+  "sdk": {
+   "allowPrerelease" : true,
+   "version" : "3.1.400-preview-015203"
+  }
+}
diff --git a/sandbox/BenchmarkVsReleasedVersion/BenchmarkVsReleasedVersion.csproj b/sandbox/BenchmarkVsReleasedVersion/BenchmarkVsReleasedVersion.csproj
index 7e34c82..4e03240 100644
--- a/sandbox/BenchmarkVsReleasedVersion/BenchmarkVsReleasedVersion.csproj
+++ b/sandbox/BenchmarkVsReleasedVersion/BenchmarkVsReleasedVersion.csproj
@@ -9,7 +9,7 @@

     <ItemGroup>
       <PackageReference Include="System.Runtime.CompilerServices.Unsafe" Version="4.7.1" Condition="$(TargetFramework) != 'netstandard3.1'" />
-      <PackageReference Include="ZString" Version="2.1.3" />
+      <PackageReference Include="ZString" Version="2.2.0" />
       <PackageReference Include="BenchmarkDotNet" Version="0.12.1" />
     </ItemGroup>

diff --git a/sandbox/BenchmarkVsReleasedVersion/BuiltinTypesBenchmark.cs b/sandbox/BenchmarkVsReleasedVersion/BuiltinTypesBenchmark.cs
index 20b8ab4..5de265c 100644
--- a/sandbox/BenchmarkVsReleasedVersion/BuiltinTypesBenchmark.cs
+++ b/sandbox/BenchmarkVsReleasedVersion/BuiltinTypesBenchmark.cs
@@ -96,13 +96,13 @@ namespace BenchmarkVsReleasedVersion
                 _byte, _dt, _dto, _decimal, _double, _guid, _short, _float, _ts, _uint, _ulong, _null, _string, _bool, _enum, _char);
         }

-        [BenchmarkCategory("CreatePreparedFormat"), Benchmark(Baseline = true)]
+        //[BenchmarkCategory("CreatePreparedFormat"), Benchmark(Baseline = true)]
         public object CreatePreparedFormat_()
         {
             return new PF16(_format);
         }

-        [BenchmarkCategory("CreatePreparedFormat"), Benchmark]
+        //[BenchmarkCategory("CreatePreparedFormat"), Benchmark]
         public object CreatePreparedFormatN()
         {
             return new NPF16(_format);
@@ -122,14 +122,14 @@ namespace BenchmarkVsReleasedVersion
                 _byte, _dt, _dto, _decimal, _double, _guid, _short, _float, _ts, _uint, _ulong, _null, _string, _bool, _enum, _char);
         }

-        [BenchmarkCategory("Utf8PreparedFormat"), Benchmark(Baseline = true)]
+        //[BenchmarkCategory("Utf8PreparedFormat"), Benchmark(Baseline = true)]
         public string Utf8PreparedFormat_()
         {
             return _utf8preparedFormat_.Format(
                 _byte, _dt, _dto, _decimal, _double, _guid, _short, _float, _ts, _uint, _ulong, _null, _string, _bool, _enum, _char);
         }

-        [BenchmarkCategory("Utf8PreparedFormat"), Benchmark]
+        //[BenchmarkCategory("Utf8PreparedFormat"), Benchmark]
         public string Utf8PreparedFormatN()
         {
             return _utf8preparedFormatN.Format(
@@ -154,7 +154,7 @@ namespace BenchmarkVsReleasedVersion
             return zsh.Length;
         }

-        [BenchmarkCategory("Utf8StringBuilderAppendFormat"), Benchmark(Baseline = true)]
+        //[BenchmarkCategory("Utf8StringBuilderAppendFormat"), Benchmark(Baseline = true)]
         public int Utf8StringBuilderAppendFormat_()
         {
             using var zsh = ZString.CreateUtf8StringBuilder();
@@ -163,7 +163,7 @@ namespace BenchmarkVsReleasedVersion
             return zsh.Length;
         }

-        [BenchmarkCategory("Utf8StringBuilderAppendFormat"), Benchmark]
+        //[BenchmarkCategory("Utf8StringBuilderAppendFormat"), Benchmark]
         public int Utf8StringBuilderAppendFormatN()
         {
             using var zsh = NZString.CreateUtf8StringBuilder();
diff --git a/sandbox/BenchmarkVsReleasedVersion/FormatBenchmark.cs b/sandbox/BenchmarkVsReleasedVersion/FormatBenchmark.cs
index 14a49c2..a6326cb 100644
--- a/sandbox/BenchmarkVsReleasedVersion/FormatBenchmark.cs
+++ b/sandbox/BenchmarkVsReleasedVersion/FormatBenchmark.cs
@@ -61,13 +61,13 @@ namespace BenchmarkVsReleasedVersion
             return NZString.Format(FormatString, x, y);
         }

-        [BenchmarkCategory("CreatePreparedFormat"), Benchmark(Baseline = true)]
+        //[BenchmarkCategory("CreatePreparedFormat"), Benchmark(Baseline = true)]
         public object CreatePreparedFormat_()
         {
             return new Utf16PreparedFormat<int, int>(FormatString);
         }

-        [BenchmarkCategory("CreatePreparedFormat"), Benchmark]
+        //[BenchmarkCategory("CreatePreparedFormat"), Benchmark]
         public object CreatePreparedFormatN()
         {
             return new NewZString::Cysharp.Text.Utf16PreparedFormat<int, int>(FormatString);
@@ -85,13 +85,13 @@ namespace BenchmarkVsReleasedVersion
             return _utf16preparedFormatN.Format(x, y);
         }

-        [BenchmarkCategory("Utf8PreparedFormat"), Benchmark(Baseline = true)]
+        //[BenchmarkCategory("Utf8PreparedFormat"), Benchmark(Baseline = true)]
         public string Utf8PreparedFormat_()
         {
             return _utf8preparedFormat_.Format(x, y);
         }

-        [BenchmarkCategory("Utf8PreparedFormat"), Benchmark]
+        //[BenchmarkCategory("Utf8PreparedFormat"), Benchmark]
         public string Utf8PreparedFormatN()
         {
             return _utf8preparedFormatN.Format(x, y);
@@ -113,7 +113,7 @@ namespace BenchmarkVsReleasedVersion
             return zsh.Length;
         }

-        [BenchmarkCategory("Utf8StringBuilderAppendFormat"), Benchmark(Baseline = true)]
+        //[BenchmarkCategory("Utf8StringBuilderAppendFormat"), Benchmark(Baseline = true)]
         public int Utf8StringBuilderAppendFormat_()
         {
             using var zsh = ZString.CreateUtf8StringBuilder();
@@ -121,7 +121,7 @@ namespace BenchmarkVsReleasedVersion
             return zsh.Length;
         }

-        [BenchmarkCategory("Utf8StringBuilderAppendFormat"), Benchmark]
+        //[BenchmarkCategory("Utf8StringBuilderAppendFormat"), Benchmark]
         public int Utf8StringBuilderAppendFormatN()
         {
             using var zsh = NZString.CreateUtf8StringBuilder();
diff --git a/sandbox/BenchmarkVsReleasedVersion/Program.cs b/sandbox/BenchmarkVsReleasedVersion/Program.cs
index 45ee687..8574158 100644
--- a/sandbox/BenchmarkVsReleasedVersion/Program.cs
+++ b/sandbox/BenchmarkVsReleasedVersion/Program.cs
@@ -19,7 +19,8 @@ namespace BenchmarkVsReleasedVersion
         public BenchmarkConfig()
         {
             AddDiagnoser(MemoryDiagnoser.Default);
-            AddJob(Job.ShortRun.WithWarmupCount(1).WithIterationCount(1));
+            //AddJob(Job.ShortRun.WithWarmupCount(1).WithIterationCount(1));
+            AddJob(Job.Default);
         }
     }

diff --git a/src/ZString.Unity/Assets/Scripts/ZString/PreparedFormat.cs b/src/ZString.Unity/Assets/Scripts/ZString/PreparedFormat.cs
index c343a57..d7232bd 100644
--- a/src/ZString.Unity/Assets/Scripts/ZString/PreparedFormat.cs
+++ b/src/ZString.Unity/Assets/Scripts/ZString/PreparedFormat.cs
@@ -4,7 +4,7 @@ using System.Buffers;

 namespace Cysharp.Text
 {
-    public sealed partial class Utf16PreparedFormat<T1>
+    public sealed unsafe partial class Utf16PreparedFormat<T1>
     {
         public string FormatString { get; }
         public int MinSize { get; }
@@ -80,7 +80,7 @@ namespace Cysharp.Text
         }
     }

-    public sealed partial class Utf16PreparedFormat<T1, T2>
+    public sealed unsafe partial class Utf16PreparedFormat<T1, T2>
     {
         public string FormatString { get; }
         public int MinSize { get; }
@@ -171,7 +171,7 @@ namespace Cysharp.Text
         }
     }

-    public sealed partial class Utf16PreparedFormat<T1, T2, T3>
+    public sealed unsafe partial class Utf16PreparedFormat<T1, T2, T3>
     {
         public string FormatString { get; }
         public int MinSize { get; }
@@ -277,7 +277,7 @@ namespace Cysharp.Text
         }
     }

-    public sealed partial class Utf16PreparedFormat<T1, T2, T3, T4>
+    public sealed unsafe partial class Utf16PreparedFormat<T1, T2, T3, T4>
     {
         public string FormatString { get; }
         public int MinSize { get; }
@@ -398,7 +398,7 @@ namespace Cysharp.Text
         }
     }

-    public sealed partial class Utf16PreparedFormat<T1, T2, T3, T4, T5>
+    public sealed unsafe partial class Utf16PreparedFormat<T1, T2, T3, T4, T5>
     {
         public string FormatString { get; }
         public int MinSize { get; }
@@ -534,7 +534,7 @@ namespace Cysharp.Text
         }
     }

-    public sealed partial class Utf16PreparedFormat<T1, T2, T3, T4, T5, T6>
+    public sealed unsafe partial class Utf16PreparedFormat<T1, T2, T3, T4, T5, T6>
     {
         public string FormatString { get; }
         public int MinSize { get; }
@@ -685,7 +685,7 @@ namespace Cysharp.Text
         }
     }

-    public sealed partial class Utf16PreparedFormat<T1, T2, T3, T4, T5, T6, T7>
+    public sealed unsafe partial class Utf16PreparedFormat<T1, T2, T3, T4, T5, T6, T7>
     {
         public string FormatString { get; }
         public int MinSize { get; }
@@ -851,7 +851,7 @@ namespace Cysharp.Text
         }
     }

-    public sealed partial class Utf16PreparedFormat<T1, T2, T3, T4, T5, T6, T7, T8>
+    public sealed unsafe partial class Utf16PreparedFormat<T1, T2, T3, T4, T5, T6, T7, T8>
     {
         public string FormatString { get; }
         public int MinSize { get; }
@@ -1032,7 +1032,7 @@ namespace Cysharp.Text
         }
     }

-    public sealed partial class Utf16PreparedFormat<T1, T2, T3, T4, T5, T6, T7, T8, T9>
+    public sealed unsafe partial class Utf16PreparedFormat<T1, T2, T3, T4, T5, T6, T7, T8, T9>
     {
         public string FormatString { get; }
         public int MinSize { get; }
@@ -1228,7 +1228,7 @@ namespace Cysharp.Text
         }
     }

-    public sealed partial class Utf16PreparedFormat<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10>
+    public sealed unsafe partial class Utf16PreparedFormat<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10>
     {
         public string FormatString { get; }
         public int MinSize { get; }
@@ -1439,7 +1439,7 @@ namespace Cysharp.Text
         }
     }

-    public sealed partial class Utf16PreparedFormat<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11>
+    public sealed unsafe partial class Utf16PreparedFormat<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11>
     {
         public string FormatString { get; }
         public int MinSize { get; }
@@ -1665,7 +1665,7 @@ namespace Cysharp.Text
         }
     }

-    public sealed partial class Utf16PreparedFormat<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12>
+    public sealed unsafe partial class Utf16PreparedFormat<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12>
     {
         public string FormatString { get; }
         public int MinSize { get; }
@@ -1906,7 +1906,7 @@ namespace Cysharp.Text
         }
     }

-    public sealed partial class Utf16PreparedFormat<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13>
+    public sealed unsafe partial class Utf16PreparedFormat<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13>
     {
         public string FormatString { get; }
         public int MinSize { get; }
@@ -2162,7 +2162,7 @@ namespace Cysharp.Text
         }
     }

-    public sealed partial class Utf16PreparedFormat<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14>
+    public sealed unsafe partial class Utf16PreparedFormat<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14>
     {
         public string FormatString { get; }
         public int MinSize { get; }
@@ -2433,7 +2433,7 @@ namespace Cysharp.Text
         }
     }

-    public sealed partial class Utf16PreparedFormat<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15>
+    public sealed unsafe partial class Utf16PreparedFormat<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15>
     {
         public string FormatString { get; }
         public int MinSize { get; }
@@ -2719,7 +2719,7 @@ namespace Cysharp.Text
         }
     }

-    public sealed partial class Utf16PreparedFormat<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16>
+    public sealed unsafe partial class Utf16PreparedFormat<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16>
     {
         public string FormatString { get; }
         public int MinSize { get; }
@@ -3020,7 +3020,7 @@ namespace Cysharp.Text
         }
     }

-    public sealed partial class Utf8PreparedFormat<T1>
+    public sealed unsafe partial class Utf8PreparedFormat<T1>
     {
         public string FormatString { get; }
         public int MinSize { get; }
@@ -3097,7 +3097,7 @@ namespace Cysharp.Text
         }
     }

-    public sealed partial class Utf8PreparedFormat<T1, T2>
+    public sealed unsafe partial class Utf8PreparedFormat<T1, T2>
     {
         public string FormatString { get; }
         public int MinSize { get; }
@@ -3189,7 +3189,7 @@ namespace Cysharp.Text
         }
     }

-    public sealed partial class Utf8PreparedFormat<T1, T2, T3>
+    public sealed unsafe partial class Utf8PreparedFormat<T1, T2, T3>
     {
         public string FormatString { get; }
         public int MinSize { get; }
@@ -3296,7 +3296,7 @@ namespace Cysharp.Text
         }
     }

-    public sealed partial class Utf8PreparedFormat<T1, T2, T3, T4>
+    public sealed unsafe partial class Utf8PreparedFormat<T1, T2, T3, T4>
     {
         public string FormatString { get; }
         public int MinSize { get; }
@@ -3418,7 +3418,7 @@ namespace Cysharp.Text
         }
     }

-    public sealed partial class Utf8PreparedFormat<T1, T2, T3, T4, T5>
+    public sealed unsafe partial class Utf8PreparedFormat<T1, T2, T3, T4, T5>
     {
         public string FormatString { get; }
         public int MinSize { get; }
@@ -3555,7 +3555,7 @@ namespace Cysharp.Text
         }
     }

-    public sealed partial class Utf8PreparedFormat<T1, T2, T3, T4, T5, T6>
+    public sealed unsafe partial class Utf8PreparedFormat<T1, T2, T3, T4, T5, T6>
     {
         public string FormatString { get; }
         public int MinSize { get; }
@@ -3707,7 +3707,7 @@ namespace Cysharp.Text
         }
     }

-    public sealed partial class Utf8PreparedFormat<T1, T2, T3, T4, T5, T6, T7>
+    public sealed unsafe partial class Utf8PreparedFormat<T1, T2, T3, T4, T5, T6, T7>
     {
         public string FormatString { get; }
         public int MinSize { get; }
@@ -3874,7 +3874,7 @@ namespace Cysharp.Text
         }
     }

-    public sealed partial class Utf8PreparedFormat<T1, T2, T3, T4, T5, T6, T7, T8>
+    public sealed unsafe partial class Utf8PreparedFormat<T1, T2, T3, T4, T5, T6, T7, T8>
     {
         public string FormatString { get; }
         public int MinSize { get; }
@@ -4056,7 +4056,7 @@ namespace Cysharp.Text
         }
     }

-    public sealed partial class Utf8PreparedFormat<T1, T2, T3, T4, T5, T6, T7, T8, T9>
+    public sealed unsafe partial class Utf8PreparedFormat<T1, T2, T3, T4, T5, T6, T7, T8, T9>
     {
         public string FormatString { get; }
         public int MinSize { get; }
@@ -4253,7 +4253,7 @@ namespace Cysharp.Text
         }
     }

-    public sealed partial class Utf8PreparedFormat<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10>
+    public sealed unsafe partial class Utf8PreparedFormat<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10>
     {
         public string FormatString { get; }
         public int MinSize { get; }
@@ -4465,7 +4465,7 @@ namespace Cysharp.Text
         }
     }

-    public sealed partial class Utf8PreparedFormat<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11>
+    public sealed unsafe partial class Utf8PreparedFormat<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11>
     {
         public string FormatString { get; }
         public int MinSize { get; }
@@ -4692,7 +4692,7 @@ namespace Cysharp.Text
         }
     }

-    public sealed partial class Utf8PreparedFormat<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12>
+    public sealed unsafe partial class Utf8PreparedFormat<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12>
     {
         public string FormatString { get; }
         public int MinSize { get; }
@@ -4934,7 +4934,7 @@ namespace Cysharp.Text
         }
     }

-    public sealed partial class Utf8PreparedFormat<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13>
+    public sealed unsafe partial class Utf8PreparedFormat<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13>
     {
         public string FormatString { get; }
         public int MinSize { get; }
@@ -5191,7 +5191,7 @@ namespace Cysharp.Text
         }
     }

-    public sealed partial class Utf8PreparedFormat<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14>
+    public sealed unsafe partial class Utf8PreparedFormat<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14>
     {
         public string FormatString { get; }
         public int MinSize { get; }
@@ -5463,7 +5463,7 @@ namespace Cysharp.Text
         }
     }

-    public sealed partial class Utf8PreparedFormat<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15>
+    public sealed unsafe partial class Utf8PreparedFormat<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15>
     {
         public string FormatString { get; }
         public int MinSize { get; }
@@ -5750,7 +5750,7 @@ namespace Cysharp.Text
         }
     }

-    public sealed partial class Utf8PreparedFormat<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16>
+    public sealed unsafe partial class Utf8PreparedFormat<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16>
     {
         public string FormatString { get; }
         public int MinSize { get; }
diff --git a/src/ZString.Unity/Assets/Scripts/ZString/Utf16/Utf16ValueStringBuilder.AppendFormat.cs b/src/ZString.Unity/Assets/Scripts/ZString/Utf16/Utf16ValueStringBuilder.AppendFormat.cs
index 515186d..9afcc41 100644
--- a/src/ZString.Unity/Assets/Scripts/ZString/Utf16/Utf16ValueStringBuilder.AppendFormat.cs
+++ b/src/ZString.Unity/Assets/Scripts/ZString/Utf16/Utf16ValueStringBuilder.AppendFormat.cs
@@ -2,7 +2,7 @@

 namespace Cysharp.Text
 {
-    public partial struct Utf16ValueStringBuilder
+    public unsafe partial struct Utf16ValueStringBuilder
     {
         /// <summary>Appends the string returned by processing a composite format string, each format item is replaced by the string representation of arguments.</summary>
         public void AppendFormat<T1>(string format, T1 arg1)
diff --git a/src/ZString.Unity/Assets/Scripts/ZString/Utf16/Utf16ValueStringBuilder.CreateFormatter.cs b/src/ZString.Unity/Assets/Scripts/ZString/Utf16/Utf16ValueStringBuilder.CreateFormatter.cs
index 8d939c7..77b85ea 100644
--- a/src/ZString.Unity/Assets/Scripts/ZString/Utf16/Utf16ValueStringBuilder.CreateFormatter.cs
+++ b/src/ZString.Unity/Assets/Scripts/ZString/Utf16/Utf16ValueStringBuilder.CreateFormatter.cs
@@ -1,146 +1,167 @@
 ・ソusing System;
+using System.Runtime.CompilerServices;

 namespace Cysharp.Text
 {
     public partial struct Utf16ValueStringBuilder
     {
-        static object CreateFormatter(Type type)
+        static unsafe void RegisterPrimitives()
         {
-            if (type == typeof(System.SByte))
+            [MethodImpl(MethodImplOptions.AggressiveInlining)]
+            static bool SByteFunc(System.SByte x, Span<char> dest, out int written, ReadOnlySpan<char> format)
             {
-                return new TryFormat<System.SByte>((System.SByte x, Span<char> dest, out int written, ReadOnlySpan<char> format) => format.Length == 0 ? FastNumberWriter.TryWriteInt64(dest, out written, x) : x.TryFormat(dest, out written, format));
+                return format.Length == 0 ? FastNumberWriter.TryWriteInt64(dest, out written, x) : x.TryFormat(dest, out written, format);
             }
-            if (type == typeof(System.Int16))
-            {
-                return new TryFormat<System.Int16>((System.Int16 x, Span<char> dest, out int written, ReadOnlySpan<char> format) => format.Length == 0 ? FastNumberWriter.TryWriteInt64(dest, out written, x) : x.TryFormat(dest, out written, format));
-            }
-            if (type == typeof(System.Int32))
-            {
-                return new TryFormat<System.Int32>((System.Int32 x, Span<char> dest, out int written, ReadOnlySpan<char> format) => format.Length == 0 ? FastNumberWriter.TryWriteInt64(dest, out written, x) : x.TryFormat(dest, out written, format));
-            }
-            if (type == typeof(System.Int64))
-            {
-                return new TryFormat<System.Int64>((System.Int64 x, Span<char> dest, out int written, ReadOnlySpan<char> format) => format.Length == 0 ? FastNumberWriter.TryWriteInt64(dest, out written, x) : x.TryFormat(dest, out written, format));
-            }
-            if (type == typeof(System.Byte))
-            {
-                return new TryFormat<System.Byte>((System.Byte x, Span<char> dest, out int written, ReadOnlySpan<char> format) => format.Length == 0 ? FastNumberWriter.TryWriteUInt64(dest, out written, x) : x.TryFormat(dest, out written, format));
-            }
-            if (type == typeof(System.UInt16))
-            {
-                return new TryFormat<System.UInt16>((System.UInt16 x, Span<char> dest, out int written, ReadOnlySpan<char> format) => format.Length == 0 ? FastNumberWriter.TryWriteUInt64(dest, out written, x) : x.TryFormat(dest, out written, format));
-            }
-            if (type == typeof(System.UInt32))
-            {
-                return new TryFormat<System.UInt32>((System.UInt32 x, Span<char> dest, out int written, ReadOnlySpan<char> format) => format.Length == 0 ? FastNumberWriter.TryWriteUInt64(dest, out written, x) : x.TryFormat(dest, out written, format));
-            }
-            if (type == typeof(System.UInt64))
-            {
-                return new TryFormat<System.UInt64>((System.UInt64 x, Span<char> dest, out int written, ReadOnlySpan<char> format) => format.Length == 0 ? FastNumberWriter.TryWriteUInt64(dest, out written, x) : x.TryFormat(dest, out written, format));
-            }
-            if (type == typeof(System.Single))
-            {
-                return new TryFormat<System.Single>((System.Single x, Span<char> dest, out int written, ReadOnlySpan<char> format) => x.TryFormat(dest, out written, format));
-            }
-            if (type == typeof(System.Double))
-            {
-                return new TryFormat<System.Double>((System.Double x, Span<char> dest, out int written, ReadOnlySpan<char> format) => x.TryFormat(dest, out written, format));
-            }
-            if (type == typeof(System.TimeSpan))
-            {
-                return new TryFormat<System.TimeSpan>((System.TimeSpan x, Span<char> dest, out int written, ReadOnlySpan<char> format) => x.TryFormat(dest, out written, format));
-            }
-            if (type == typeof(System.DateTime))
-            {
-                return new TryFormat<System.DateTime>((System.DateTime x, Span<char> dest, out int written, ReadOnlySpan<char> format) => x.TryFormat(dest, out written, format));
-            }
-            if (type == typeof(System.DateTimeOffset))
-            {
-                return new TryFormat<System.DateTimeOffset>((System.DateTimeOffset x, Span<char> dest, out int written, ReadOnlySpan<char> format) => x.TryFormat(dest, out written, format));
-            }
-            if (type == typeof(System.Decimal))
-            {
-                return new TryFormat<System.Decimal>((System.Decimal x, Span<char> dest, out int written, ReadOnlySpan<char> format) => x.TryFormat(dest, out written, format));
-            }
-            if (type == typeof(System.Guid))
-            {
-                return new TryFormat<System.Guid>((System.Guid x, Span<char> dest, out int written, ReadOnlySpan<char> format) => x.TryFormat(dest, out written, format));
-            }
-            if (type == typeof(System.Byte?))
-            {
-                return CreateNullableFormatter<System.Byte>();
-            }
-            if (type == typeof(System.DateTime?))
+            FormatterCache<System.SByte>.TryFormatDelegate = &SByteFunc;
+
+            [MethodImpl(MethodImplOptions.AggressiveInlining)]
+            static bool Int16Func(System.Int16 x, Span<char> dest, out int written, ReadOnlySpan<char> format)
             {
-                return CreateNullableFormatter<System.DateTime>();
+                return format.Length == 0 ? FastNumberWriter.TryWriteInt64(dest, out written, x) : x.TryFormat(dest, out written, format);
             }
-            if (type == typeof(System.DateTimeOffset?))
+            FormatterCache<System.Int16>.TryFormatDelegate = &Int16Func;
+
+            [MethodImpl(MethodImplOptions.AggressiveInlining)]
+            static bool Int32Func(System.Int32 x, Span<char> dest, out int written, ReadOnlySpan<char> format)
             {
-                return CreateNullableFormatter<System.DateTimeOffset>();
+                return format.Length == 0 ? FastNumberWriter.TryWriteInt64(dest, out written, x) : x.TryFormat(dest, out written, format);
             }
-            if (type == typeof(System.Decimal?))
+            FormatterCache<System.Int32>.TryFormatDelegate = &Int32Func;
+
+            [MethodImpl(MethodImplOptions.AggressiveInlining)]
+            static bool Int64Func(System.Int64 x, Span<char> dest, out int written, ReadOnlySpan<char> format)
             {
-                return CreateNullableFormatter<System.Decimal>();
+                return format.Length == 0 ? FastNumberWriter.TryWriteInt64(dest, out written, x) : x.TryFormat(dest, out written, format);
             }
-            if (type == typeof(System.Double?))
+            FormatterCache<System.Int64>.TryFormatDelegate = &Int64Func;
+
+            [MethodImpl(MethodImplOptions.AggressiveInlining)]
+            static bool ByteFunc(System.Byte x, Span<char> dest, out int written, ReadOnlySpan<char> format)
             {
-                return CreateNullableFormatter<System.Double>();
+                return format.Length == 0 ? FastNumberWriter.TryWriteUInt64(dest, out written, x) : x.TryFormat(dest, out written, format);
             }
-            if (type == typeof(System.Int16?))
+            FormatterCache<System.Byte>.TryFormatDelegate = &ByteFunc;
+
+            [MethodImpl(MethodImplOptions.AggressiveInlining)]
+            static bool UInt16Func(System.UInt16 x, Span<char> dest, out int written, ReadOnlySpan<char> format)
             {
-                return CreateNullableFormatter<System.Int16>();
+                return format.Length == 0 ? FastNumberWriter.TryWriteUInt64(dest, out written, x) : x.TryFormat(dest, out written, format);
             }
-            if (type == typeof(System.Int32?))
+            FormatterCache<System.UInt16>.TryFormatDelegate = &UInt16Func;
+
+            [MethodImpl(MethodImplOptions.AggressiveInlining)]
+            static bool UInt32Func(System.UInt32 x, Span<char> dest, out int written, ReadOnlySpan<char> format)
             {
-                return CreateNullableFormatter<System.Int32>();
+                return format.Length == 0 ? FastNumberWriter.TryWriteUInt64(dest, out written, x) : x.TryFormat(dest, out written, format);
             }
-            if (type == typeof(System.Int64?))
+            FormatterCache<System.UInt32>.TryFormatDelegate = &UInt32Func;
+
+            [MethodImpl(MethodImplOptions.AggressiveInlining)]
+            static bool UInt64Func(System.UInt64 x, Span<char> dest, out int written, ReadOnlySpan<char> format)
             {
-                return CreateNullableFormatter<System.Int64>();
+                return format.Length == 0 ? FastNumberWriter.TryWriteUInt64(dest, out written, x) : x.TryFormat(dest, out written, format);
             }
-            if (type == typeof(System.SByte?))
+            FormatterCache<System.UInt64>.TryFormatDelegate = &UInt64Func;
+
+            [MethodImpl(MethodImplOptions.AggressiveInlining)]
+            static bool SingleFunc(System.Single x, Span<char> dest, out int written, ReadOnlySpan<char> format)
             {
-                return CreateNullableFormatter<System.SByte>();
+                return x.TryFormat(dest, out written, format);
             }
-            if (type == typeof(System.Single?))
+            FormatterCache<System.Single>.TryFormatDelegate = &SingleFunc;
+
+            [MethodImpl(MethodImplOptions.AggressiveInlining)]
+            static bool DoubleFunc(System.Double x, Span<char> dest, out int written, ReadOnlySpan<char> format)
             {
-                return CreateNullableFormatter<System.Single>();
+                return x.TryFormat(dest, out written, format);
             }
-            if (type == typeof(System.TimeSpan?))
+            FormatterCache<System.Double>.TryFormatDelegate = &DoubleFunc;
+
+            [MethodImpl(MethodImplOptions.AggressiveInlining)]
+            static bool TimeSpanFunc(System.TimeSpan x, Span<char> dest, out int written, ReadOnlySpan<char> format)
             {
-                return CreateNullableFormatter<System.TimeSpan>();
+                return x.TryFormat(dest, out written, format);
             }
-            if (type == typeof(System.UInt16?))
+            FormatterCache<System.TimeSpan>.TryFormatDelegate = &TimeSpanFunc;
+
+            [MethodImpl(MethodImplOptions.AggressiveInlining)]
+            static bool DateTimeFunc(System.DateTime x, Span<char> dest, out int written, ReadOnlySpan<char> format)
             {
-                return CreateNullableFormatter<System.UInt16>();
+                return x.TryFormat(dest, out written, format);
             }
-            if (type == typeof(System.UInt32?))
+            FormatterCache<System.DateTime>.TryFormatDelegate = &DateTimeFunc;
+
+            [MethodImpl(MethodImplOptions.AggressiveInlining)]
+            static bool DateTimeOffsetFunc(System.DateTimeOffset x, Span<char> dest, out int written, ReadOnlySpan<char> format)
             {
-                return CreateNullableFormatter<System.UInt32>();
+                return x.TryFormat(dest, out written, format);
             }
-            if (type == typeof(System.UInt64?))
+            FormatterCache<System.DateTimeOffset>.TryFormatDelegate = &DateTimeOffsetFunc;
+
+            [MethodImpl(MethodImplOptions.AggressiveInlining)]
+            static bool DecimalFunc(System.Decimal x, Span<char> dest, out int written, ReadOnlySpan<char> format)
             {
-                return CreateNullableFormatter<System.UInt64>();
+                return x.TryFormat(dest, out written, format);
             }
-            if (type == typeof(System.Guid?))
+            FormatterCache<System.Decimal>.TryFormatDelegate = &DecimalFunc;
+
+            [MethodImpl(MethodImplOptions.AggressiveInlining)]
+            static bool GuidFunc(System.Guid x, Span<char> dest, out int written, ReadOnlySpan<char> format)
             {
-                return CreateNullableFormatter<System.Guid>();
+                return x.TryFormat(dest, out written, format);
             }
-            if (type == typeof(System.IntPtr))
+            FormatterCache<System.Guid>.TryFormatDelegate = &GuidFunc;
+
+            FormatterCache<System.Byte?>.TryFormatDelegate = &NullableFormat;
+
+            FormatterCache<System.DateTime?>.TryFormatDelegate = &NullableFormat;
+
+            FormatterCache<System.DateTimeOffset?>.TryFormatDelegate = &NullableFormat;
+
+            FormatterCache<System.Decimal?>.TryFormatDelegate = &NullableFormat;
+
+            FormatterCache<System.Double?>.TryFormatDelegate = &NullableFormat;
+
+            FormatterCache<System.Int16?>.TryFormatDelegate = &NullableFormat;
+
+            FormatterCache<System.Int32?>.TryFormatDelegate = &NullableFormat;
+
+            FormatterCache<System.Int64?>.TryFormatDelegate = &NullableFormat;
+
+            FormatterCache<System.SByte?>.TryFormatDelegate = &NullableFormat;
+
+            FormatterCache<System.Single?>.TryFormatDelegate = &NullableFormat;
+
+            FormatterCache<System.TimeSpan?>.TryFormatDelegate = &NullableFormat;
+
+            FormatterCache<System.UInt16?>.TryFormatDelegate = &NullableFormat;
+
+            FormatterCache<System.UInt32?>.TryFormatDelegate = &NullableFormat;
+
+            FormatterCache<System.UInt64?>.TryFormatDelegate = &NullableFormat;
+
+            FormatterCache<System.Guid?>.TryFormatDelegate = &NullableFormat;
+
+            [MethodImpl(MethodImplOptions.AggressiveInlining)]
+            static bool IntPtrFunc(System.IntPtr x, Span<char> dest, out int written, ReadOnlySpan<char> format)
             {
                 // ignore format
-                return new TryFormat<System.IntPtr>((System.IntPtr x, Span<char> dest, out int written, ReadOnlySpan<char> _) => System.IntPtr.Size == 4
+                return  System.IntPtr.Size == 4
                     ? x.ToInt32().TryFormat(dest, out written, default)
-                    : x.ToInt64().TryFormat(dest, out written, default));
+                    : x.ToInt64().TryFormat(dest, out written, default);
             }
-            if (type == typeof(System.UIntPtr))
+            FormatterCache<System.IntPtr>.TryFormatDelegate = &IntPtrFunc;
+
+            [MethodImpl(MethodImplOptions.AggressiveInlining)]
+            static bool UIntPtrFunc(System.UIntPtr x, Span<char> dest, out int written, ReadOnlySpan<char> format)
             {
                 // ignore format
-                return new TryFormat<System.UIntPtr>((System.UIntPtr x, Span<char> dest, out int written, ReadOnlySpan<char> _) => System.UIntPtr.Size == 4
+                return  System.UIntPtr.Size == 4
                     ? x.ToUInt32().TryFormat(dest, out written, default)
-                    : x.ToUInt64().TryFormat(dest, out written, default));
+                    : x.ToUInt64().TryFormat(dest, out written, default);
             }
-            return null;
+            FormatterCache<System.UIntPtr>.TryFormatDelegate = &UIntPtrFunc;
+
         }
     }
 }
\ No newline at end of file
diff --git a/src/ZString.Unity/Assets/Scripts/ZString/Utf16ValueStringBuilder.cs b/src/ZString.Unity/Assets/Scripts/ZString/Utf16ValueStringBuilder.cs
index 7a4368a..1b9ad83 100644
--- a/src/ZString.Unity/Assets/Scripts/ZString/Utf16ValueStringBuilder.cs
+++ b/src/ZString.Unity/Assets/Scripts/ZString/Utf16ValueStringBuilder.cs
@@ -5,7 +5,7 @@ using System.Runtime.CompilerServices;

 namespace Cysharp.Text
 {
-    public partial struct Utf16ValueStringBuilder : IDisposable, IBufferWriter<char>, IResettableBufferWriter<char>
+    public unsafe partial struct Utf16ValueStringBuilder : IDisposable, IBufferWriter<char>, IResettableBufferWriter<char>
     {
         public delegate bool TryFormat<T>(T value, Span<char> destination, out int charsWritten, ReadOnlySpan<char> format);

@@ -32,6 +32,7 @@ namespace Cysharp.Text
                 newLine2 = newLine[1];
                 crlf = true;
             }
+            RegisterPrimitives();
         }

         [ThreadStatic]
@@ -232,7 +233,7 @@ namespace Cysharp.Text
         }

         /// <summary>Appends the string representation of a specified value to this instance.</summary>
-        public void Append<T>(T value)
+        public unsafe void Append<T>(T value)
         {
             if (!FormatterCache<T>.TryFormatDelegate(value, buffer.AsSpan(index), out var written, default))
             {
@@ -361,7 +362,7 @@ namespace Cysharp.Text
         /// are removed from this builder.
         /// </remarks>
         public void Replace(string oldValue, string newValue) => Replace(oldValue, newValue, 0, Length);
-        
+
         public void Replace(ReadOnlySpan<char> oldValue, ReadOnlySpan<char> newValue) => Replace(oldValue, newValue, 0, Length);

         /// <summary>
@@ -578,58 +579,62 @@ namespace Cysharp.Text
             index += written;
         }

+        static class CustomTryFormat<T>
+        {
+            public static TryFormat<T> formatMethod;
+            public static bool Dispatch(T value, Span<char> dest, out int written, ReadOnlySpan<char> format)
+            {
+                return formatMethod(value, dest, out written, format);
+            }
+        }
+
         /// <summary>
         /// Register custom formatter
         /// </summary>
         public static void RegisterTryFormat<T>(TryFormat<T> formatMethod)
         {
-            FormatterCache<T>.TryFormatDelegate = formatMethod;
+            CustomTryFormat<T>.formatMethod = formatMethod;
+            FormatterCache<T>.TryFormatDelegate = &CustomTryFormat<T>.Dispatch;
         }

-        static TryFormat<T?> CreateNullableFormatter<T>() where T : struct
+        static unsafe bool NullableFormat<T>(T? x, Span<char> dest, out int written, ReadOnlySpan<char> format) where T : struct
         {
-            return new TryFormat<T?>((T? x, Span<char> dest, out int written, ReadOnlySpan<char> format) =>
+            if (x == null)
             {
-                if (x == null)
-                {
-                    written = 0;
-                    return true;
-                }
-                return FormatterCache<T>.TryFormatDelegate(x.Value, dest, out written, format);
-            });
+                written = 0;
+                return true;
+            }
+            return FormatterCache<T>.TryFormatDelegate(x.Value, dest, out written, format);
         }

-        /// <summary>
-        /// Supports the Nullable type for a given struct type.
-        /// </summary>
-        public static void EnableNullableFormat<T>() where T : struct
+        public unsafe static class FormatterCache<T>
         {
-            RegisterTryFormat<T?>(CreateNullableFormatter<T>());
-        }
+            private static unsafe delegate*<T, Span<char>, out int, ReadOnlySpan<char>, bool> _TryFormatDelegate;

-        public static class FormatterCache<T>
-        {
-            public static TryFormat<T> TryFormatDelegate;
-            static FormatterCache()
+            public static delegate*<T , Span<char> , out int , ReadOnlySpan<char> , bool> TryFormatDelegate
             {
-                var formatter = (TryFormat<T>)CreateFormatter(typeof(T));
-                if (formatter == null)
+                get
                 {
-                    if (typeof(T).IsEnum)
+                    if (_TryFormatDelegate == null)
                     {
-                        formatter = new TryFormat<T>(EnumUtil<T>.TryFormatUtf16);
-                    }
-                    else if (typeof(T) == typeof(string))
-                    {
-                        formatter = new TryFormat<T>(TryFormatString);
-                    }
-                    else
-                    {
-                        formatter = new TryFormat<T>(TryFormatDefault);
+                        if (typeof(T).IsEnum)
+                        {
+                            _TryFormatDelegate = &EnumUtil<T>.TryFormatUtf16;
+                        }
+                        else if (typeof(T) == typeof(string))
+                        {
+                            _TryFormatDelegate = &TryFormatString;
+                        }
+                        else
+                        {
+                            _TryFormatDelegate = &TryFormatDefault;
+                        }
                     }
+
+                    return _TryFormatDelegate;
                 }

-                TryFormatDelegate = formatter;
+                set => _TryFormatDelegate = value;
             }

             static bool TryFormatString(T value, Span<char> dest, out int written, ReadOnlySpan<char> format)
diff --git a/src/ZString/PreparedFormat.cs b/src/ZString/PreparedFormat.cs
index c343a57..d7232bd 100644
--- a/src/ZString/PreparedFormat.cs
+++ b/src/ZString/PreparedFormat.cs
@@ -4,7 +4,7 @@ using System.Buffers;

 namespace Cysharp.Text
 {
-    public sealed partial class Utf16PreparedFormat<T1>
+    public sealed unsafe partial class Utf16PreparedFormat<T1>
     {
         public string FormatString { get; }
         public int MinSize { get; }
@@ -80,7 +80,7 @@ namespace Cysharp.Text
         }
     }

-    public sealed partial class Utf16PreparedFormat<T1, T2>
+    public sealed unsafe partial class Utf16PreparedFormat<T1, T2>
     {
         public string FormatString { get; }
         public int MinSize { get; }
@@ -171,7 +171,7 @@ namespace Cysharp.Text
         }
     }

-    public sealed partial class Utf16PreparedFormat<T1, T2, T3>
+    public sealed unsafe partial class Utf16PreparedFormat<T1, T2, T3>
     {
         public string FormatString { get; }
         public int MinSize { get; }
@@ -277,7 +277,7 @@ namespace Cysharp.Text
         }
     }

-    public sealed partial class Utf16PreparedFormat<T1, T2, T3, T4>
+    public sealed unsafe partial class Utf16PreparedFormat<T1, T2, T3, T4>
     {
         public string FormatString { get; }
         public int MinSize { get; }
@@ -398,7 +398,7 @@ namespace Cysharp.Text
         }
     }

-    public sealed partial class Utf16PreparedFormat<T1, T2, T3, T4, T5>
+    public sealed unsafe partial class Utf16PreparedFormat<T1, T2, T3, T4, T5>
     {
         public string FormatString { get; }
         public int MinSize { get; }
@@ -534,7 +534,7 @@ namespace Cysharp.Text
         }
     }

-    public sealed partial class Utf16PreparedFormat<T1, T2, T3, T4, T5, T6>
+    public sealed unsafe partial class Utf16PreparedFormat<T1, T2, T3, T4, T5, T6>
     {
         public string FormatString { get; }
         public int MinSize { get; }
@@ -685,7 +685,7 @@ namespace Cysharp.Text
         }
     }

-    public sealed partial class Utf16PreparedFormat<T1, T2, T3, T4, T5, T6, T7>
+    public sealed unsafe partial class Utf16PreparedFormat<T1, T2, T3, T4, T5, T6, T7>
     {
         public string FormatString { get; }
         public int MinSize { get; }
@@ -851,7 +851,7 @@ namespace Cysharp.Text
         }
     }

-    public sealed partial class Utf16PreparedFormat<T1, T2, T3, T4, T5, T6, T7, T8>
+    public sealed unsafe partial class Utf16PreparedFormat<T1, T2, T3, T4, T5, T6, T7, T8>
     {
         public string FormatString { get; }
         public int MinSize { get; }
@@ -1032,7 +1032,7 @@ namespace Cysharp.Text
         }
     }

-    public sealed partial class Utf16PreparedFormat<T1, T2, T3, T4, T5, T6, T7, T8, T9>
+    public sealed unsafe partial class Utf16PreparedFormat<T1, T2, T3, T4, T5, T6, T7, T8, T9>
     {
         public string FormatString { get; }
         public int MinSize { get; }
@@ -1228,7 +1228,7 @@ namespace Cysharp.Text
         }
     }

-    public sealed partial class Utf16PreparedFormat<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10>
+    public sealed unsafe partial class Utf16PreparedFormat<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10>
     {
         public string FormatString { get; }
         public int MinSize { get; }
@@ -1439,7 +1439,7 @@ namespace Cysharp.Text
         }
     }

-    public sealed partial class Utf16PreparedFormat<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11>
+    public sealed unsafe partial class Utf16PreparedFormat<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11>
     {
         public string FormatString { get; }
         public int MinSize { get; }
@@ -1665,7 +1665,7 @@ namespace Cysharp.Text
         }
     }

-    public sealed partial class Utf16PreparedFormat<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12>
+    public sealed unsafe partial class Utf16PreparedFormat<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12>
     {
         public string FormatString { get; }
         public int MinSize { get; }
@@ -1906,7 +1906,7 @@ namespace Cysharp.Text
         }
     }

-    public sealed partial class Utf16PreparedFormat<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13>
+    public sealed unsafe partial class Utf16PreparedFormat<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13>
     {
         public string FormatString { get; }
         public int MinSize { get; }
@@ -2162,7 +2162,7 @@ namespace Cysharp.Text
         }
     }

-    public sealed partial class Utf16PreparedFormat<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14>
+    public sealed unsafe partial class Utf16PreparedFormat<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14>
     {
         public string FormatString { get; }
         public int MinSize { get; }
@@ -2433,7 +2433,7 @@ namespace Cysharp.Text
         }
     }

-    public sealed partial class Utf16PreparedFormat<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15>
+    public sealed unsafe partial class Utf16PreparedFormat<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15>
     {
         public string FormatString { get; }
         public int MinSize { get; }
@@ -2719,7 +2719,7 @@ namespace Cysharp.Text
         }
     }

-    public sealed partial class Utf16PreparedFormat<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16>
+    public sealed unsafe partial class Utf16PreparedFormat<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16>
     {
         public string FormatString { get; }
         public int MinSize { get; }
@@ -3020,7 +3020,7 @@ namespace Cysharp.Text
         }
     }

-    public sealed partial class Utf8PreparedFormat<T1>
+    public sealed unsafe partial class Utf8PreparedFormat<T1>
     {
         public string FormatString { get; }
         public int MinSize { get; }
@@ -3097,7 +3097,7 @@ namespace Cysharp.Text
         }
     }

-    public sealed partial class Utf8PreparedFormat<T1, T2>
+    public sealed unsafe partial class Utf8PreparedFormat<T1, T2>
     {
         public string FormatString { get; }
         public int MinSize { get; }
@@ -3189,7 +3189,7 @@ namespace Cysharp.Text
         }
     }

-    public sealed partial class Utf8PreparedFormat<T1, T2, T3>
+    public sealed unsafe partial class Utf8PreparedFormat<T1, T2, T3>
     {
         public string FormatString { get; }
         public int MinSize { get; }
@@ -3296,7 +3296,7 @@ namespace Cysharp.Text
         }
     }

-    public sealed partial class Utf8PreparedFormat<T1, T2, T3, T4>
+    public sealed unsafe partial class Utf8PreparedFormat<T1, T2, T3, T4>
     {
         public string FormatString { get; }
         public int MinSize { get; }
@@ -3418,7 +3418,7 @@ namespace Cysharp.Text
         }
     }

-    public sealed partial class Utf8PreparedFormat<T1, T2, T3, T4, T5>
+    public sealed unsafe partial class Utf8PreparedFormat<T1, T2, T3, T4, T5>
     {
         public string FormatString { get; }
         public int MinSize { get; }
@@ -3555,7 +3555,7 @@ namespace Cysharp.Text
         }
     }

-    public sealed partial class Utf8PreparedFormat<T1, T2, T3, T4, T5, T6>
+    public sealed unsafe partial class Utf8PreparedFormat<T1, T2, T3, T4, T5, T6>
     {
         public string FormatString { get; }
         public int MinSize { get; }
@@ -3707,7 +3707,7 @@ namespace Cysharp.Text
         }
     }

-    public sealed partial class Utf8PreparedFormat<T1, T2, T3, T4, T5, T6, T7>
+    public sealed unsafe partial class Utf8PreparedFormat<T1, T2, T3, T4, T5, T6, T7>
     {
         public string FormatString { get; }
         public int MinSize { get; }
@@ -3874,7 +3874,7 @@ namespace Cysharp.Text
         }
     }

-    public sealed partial class Utf8PreparedFormat<T1, T2, T3, T4, T5, T6, T7, T8>
+    public sealed unsafe partial class Utf8PreparedFormat<T1, T2, T3, T4, T5, T6, T7, T8>
     {
         public string FormatString { get; }
         public int MinSize { get; }
@@ -4056,7 +4056,7 @@ namespace Cysharp.Text
         }
     }

-    public sealed partial class Utf8PreparedFormat<T1, T2, T3, T4, T5, T6, T7, T8, T9>
+    public sealed unsafe partial class Utf8PreparedFormat<T1, T2, T3, T4, T5, T6, T7, T8, T9>
     {
         public string FormatString { get; }
         public int MinSize { get; }
@@ -4253,7 +4253,7 @@ namespace Cysharp.Text
         }
     }

-    public sealed partial class Utf8PreparedFormat<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10>
+    public sealed unsafe partial class Utf8PreparedFormat<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10>
     {
         public string FormatString { get; }
         public int MinSize { get; }
@@ -4465,7 +4465,7 @@ namespace Cysharp.Text
         }
     }

-    public sealed partial class Utf8PreparedFormat<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11>
+    public sealed unsafe partial class Utf8PreparedFormat<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11>
     {
         public string FormatString { get; }
         public int MinSize { get; }
@@ -4692,7 +4692,7 @@ namespace Cysharp.Text
         }
     }

-    public sealed partial class Utf8PreparedFormat<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12>
+    public sealed unsafe partial class Utf8PreparedFormat<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12>
     {
         public string FormatString { get; }
         public int MinSize { get; }
@@ -4934,7 +4934,7 @@ namespace Cysharp.Text
         }
     }

-    public sealed partial class Utf8PreparedFormat<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13>
+    public sealed unsafe partial class Utf8PreparedFormat<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13>
     {
         public string FormatString { get; }
         public int MinSize { get; }
@@ -5191,7 +5191,7 @@ namespace Cysharp.Text
         }
     }

-    public sealed partial class Utf8PreparedFormat<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14>
+    public sealed unsafe partial class Utf8PreparedFormat<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14>
     {
         public string FormatString { get; }
         public int MinSize { get; }
@@ -5463,7 +5463,7 @@ namespace Cysharp.Text
         }
     }

-    public sealed partial class Utf8PreparedFormat<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15>
+    public sealed unsafe partial class Utf8PreparedFormat<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15>
     {
         public string FormatString { get; }
         public int MinSize { get; }
@@ -5750,7 +5750,7 @@ namespace Cysharp.Text
         }
     }

-    public sealed partial class Utf8PreparedFormat<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16>
+    public sealed unsafe partial class Utf8PreparedFormat<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16>
     {
         public string FormatString { get; }
         public int MinSize { get; }
diff --git a/src/ZString/PreparedFormat.tt b/src/ZString/PreparedFormat.tt
index 4087bea..e3af3be 100644
--- a/src/ZString/PreparedFormat.tt
+++ b/src/ZString/PreparedFormat.tt
@@ -13,7 +13,7 @@ namespace Cysharp.Text
 {
 <# foreach(var utf in utfTypes) { var isUtf16 = (utf == "Utf16"); #>
 <# for(var i = 1; i <= TypeParamMax; i++) { #>
-    public sealed partial class <#= utf #>PreparedFormat<<#= CreateTypeArgument(i) #>>
+    public sealed unsafe partial class <#= utf #>PreparedFormat<<#= CreateTypeArgument(i) #>>
     {
         public string FormatString { get; }
         public int MinSize { get; }
diff --git a/src/ZString/Utf16/Utf16ValueStringBuilder.AppendFormat.cs b/src/ZString/Utf16/Utf16ValueStringBuilder.AppendFormat.cs
index 515186d..9afcc41 100644
--- a/src/ZString/Utf16/Utf16ValueStringBuilder.AppendFormat.cs
+++ b/src/ZString/Utf16/Utf16ValueStringBuilder.AppendFormat.cs
@@ -2,7 +2,7 @@

 namespace Cysharp.Text
 {
-    public partial struct Utf16ValueStringBuilder
+    public unsafe partial struct Utf16ValueStringBuilder
     {
         /// <summary>Appends the string returned by processing a composite format string, each format item is replaced by the string representation of arguments.</summary>
         public void AppendFormat<T1>(string format, T1 arg1)
diff --git a/src/ZString/Utf16/Utf16ValueStringBuilder.AppendFormat.tt b/src/ZString/Utf16/Utf16ValueStringBuilder.AppendFormat.tt
index 12b53b5..474d8b0 100644
--- a/src/ZString/Utf16/Utf16ValueStringBuilder.AppendFormat.tt
+++ b/src/ZString/Utf16/Utf16ValueStringBuilder.AppendFormat.tt
@@ -9,7 +9,7 @@ using System;

 namespace Cysharp.Text
 {
-    public partial struct Utf16ValueStringBuilder
+    public unsafe partial struct Utf16ValueStringBuilder
     {
 <# for(var i = 1; i <= TypeParamMax; i++) { #>
         /// <summary>Appends the string returned by processing a composite format string, each format item is replaced by the string representation of arguments.</summary>
diff --git a/src/ZString/Utf16/Utf16ValueStringBuilder.CreateFormatter.cs b/src/ZString/Utf16/Utf16ValueStringBuilder.CreateFormatter.cs
index 8d939c7..77b85ea 100644
--- a/src/ZString/Utf16/Utf16ValueStringBuilder.CreateFormatter.cs
+++ b/src/ZString/Utf16/Utf16ValueStringBuilder.CreateFormatter.cs
@@ -1,146 +1,167 @@
 ・ソusing System;
+using System.Runtime.CompilerServices;

 namespace Cysharp.Text
 {
     public partial struct Utf16ValueStringBuilder
     {
-        static object CreateFormatter(Type type)
+        static unsafe void RegisterPrimitives()
         {
-            if (type == typeof(System.SByte))
+            [MethodImpl(MethodImplOptions.AggressiveInlining)]
+            static bool SByteFunc(System.SByte x, Span<char> dest, out int written, ReadOnlySpan<char> format)
             {
-                return new TryFormat<System.SByte>((System.SByte x, Span<char> dest, out int written, ReadOnlySpan<char> format) => format.Length == 0 ? FastNumberWriter.TryWriteInt64(dest, out written, x) : x.TryFormat(dest, out written, format));
+                return format.Length == 0 ? FastNumberWriter.TryWriteInt64(dest, out written, x) : x.TryFormat(dest, out written, format);
             }
-            if (type == typeof(System.Int16))
-            {
-                return new TryFormat<System.Int16>((System.Int16 x, Span<char> dest, out int written, ReadOnlySpan<char> format) => format.Length == 0 ? FastNumberWriter.TryWriteInt64(dest, out written, x) : x.TryFormat(dest, out written, format));
-            }
-            if (type == typeof(System.Int32))
-            {
-                return new TryFormat<System.Int32>((System.Int32 x, Span<char> dest, out int written, ReadOnlySpan<char> format) => format.Length == 0 ? FastNumberWriter.TryWriteInt64(dest, out written, x) : x.TryFormat(dest, out written, format));
-            }
-            if (type == typeof(System.Int64))
-            {
-                return new TryFormat<System.Int64>((System.Int64 x, Span<char> dest, out int written, ReadOnlySpan<char> format) => format.Length == 0 ? FastNumberWriter.TryWriteInt64(dest, out written, x) : x.TryFormat(dest, out written, format));
-            }
-            if (type == typeof(System.Byte))
-            {
-                return new TryFormat<System.Byte>((System.Byte x, Span<char> dest, out int written, ReadOnlySpan<char> format) => format.Length == 0 ? FastNumberWriter.TryWriteUInt64(dest, out written, x) : x.TryFormat(dest, out written, format));
-            }
-            if (type == typeof(System.UInt16))
-            {
-                return new TryFormat<System.UInt16>((System.UInt16 x, Span<char> dest, out int written, ReadOnlySpan<char> format) => format.Length == 0 ? FastNumberWriter.TryWriteUInt64(dest, out written, x) : x.TryFormat(dest, out written, format));
-            }
-            if (type == typeof(System.UInt32))
-            {
-                return new TryFormat<System.UInt32>((System.UInt32 x, Span<char> dest, out int written, ReadOnlySpan<char> format) => format.Length == 0 ? FastNumberWriter.TryWriteUInt64(dest, out written, x) : x.TryFormat(dest, out written, format));
-            }
-            if (type == typeof(System.UInt64))
-            {
-                return new TryFormat<System.UInt64>((System.UInt64 x, Span<char> dest, out int written, ReadOnlySpan<char> format) => format.Length == 0 ? FastNumberWriter.TryWriteUInt64(dest, out written, x) : x.TryFormat(dest, out written, format));
-            }
-            if (type == typeof(System.Single))
-            {
-                return new TryFormat<System.Single>((System.Single x, Span<char> dest, out int written, ReadOnlySpan<char> format) => x.TryFormat(dest, out written, format));
-            }
-            if (type == typeof(System.Double))
-            {
-                return new TryFormat<System.Double>((System.Double x, Span<char> dest, out int written, ReadOnlySpan<char> format) => x.TryFormat(dest, out written, format));
-            }
-            if (type == typeof(System.TimeSpan))
-            {
-                return new TryFormat<System.TimeSpan>((System.TimeSpan x, Span<char> dest, out int written, ReadOnlySpan<char> format) => x.TryFormat(dest, out written, format));
-            }
-            if (type == typeof(System.DateTime))
-            {
-                return new TryFormat<System.DateTime>((System.DateTime x, Span<char> dest, out int written, ReadOnlySpan<char> format) => x.TryFormat(dest, out written, format));
-            }
-            if (type == typeof(System.DateTimeOffset))
-            {
-                return new TryFormat<System.DateTimeOffset>((System.DateTimeOffset x, Span<char> dest, out int written, ReadOnlySpan<char> format) => x.TryFormat(dest, out written, format));
-            }
-            if (type == typeof(System.Decimal))
-            {
-                return new TryFormat<System.Decimal>((System.Decimal x, Span<char> dest, out int written, ReadOnlySpan<char> format) => x.TryFormat(dest, out written, format));
-            }
-            if (type == typeof(System.Guid))
-            {
-                return new TryFormat<System.Guid>((System.Guid x, Span<char> dest, out int written, ReadOnlySpan<char> format) => x.TryFormat(dest, out written, format));
-            }
-            if (type == typeof(System.Byte?))
-            {
-                return CreateNullableFormatter<System.Byte>();
-            }
-            if (type == typeof(System.DateTime?))
+            FormatterCache<System.SByte>.TryFormatDelegate = &SByteFunc;
+
+            [MethodImpl(MethodImplOptions.AggressiveInlining)]
+            static bool Int16Func(System.Int16 x, Span<char> dest, out int written, ReadOnlySpan<char> format)
             {
-                return CreateNullableFormatter<System.DateTime>();
+                return format.Length == 0 ? FastNumberWriter.TryWriteInt64(dest, out written, x) : x.TryFormat(dest, out written, format);
             }
-            if (type == typeof(System.DateTimeOffset?))
+            FormatterCache<System.Int16>.TryFormatDelegate = &Int16Func;
+
+            [MethodImpl(MethodImplOptions.AggressiveInlining)]
+            static bool Int32Func(System.Int32 x, Span<char> dest, out int written, ReadOnlySpan<char> format)
             {
-                return CreateNullableFormatter<System.DateTimeOffset>();
+                return format.Length == 0 ? FastNumberWriter.TryWriteInt64(dest, out written, x) : x.TryFormat(dest, out written, format);
             }
-            if (type == typeof(System.Decimal?))
+            FormatterCache<System.Int32>.TryFormatDelegate = &Int32Func;
+
+            [MethodImpl(MethodImplOptions.AggressiveInlining)]
+            static bool Int64Func(System.Int64 x, Span<char> dest, out int written, ReadOnlySpan<char> format)
             {
-                return CreateNullableFormatter<System.Decimal>();
+                return format.Length == 0 ? FastNumberWriter.TryWriteInt64(dest, out written, x) : x.TryFormat(dest, out written, format);
             }
-            if (type == typeof(System.Double?))
+            FormatterCache<System.Int64>.TryFormatDelegate = &Int64Func;
+
+            [MethodImpl(MethodImplOptions.AggressiveInlining)]
+            static bool ByteFunc(System.Byte x, Span<char> dest, out int written, ReadOnlySpan<char> format)
             {
-                return CreateNullableFormatter<System.Double>();
+                return format.Length == 0 ? FastNumberWriter.TryWriteUInt64(dest, out written, x) : x.TryFormat(dest, out written, format);
             }
-            if (type == typeof(System.Int16?))
+            FormatterCache<System.Byte>.TryFormatDelegate = &ByteFunc;
+
+            [MethodImpl(MethodImplOptions.AggressiveInlining)]
+            static bool UInt16Func(System.UInt16 x, Span<char> dest, out int written, ReadOnlySpan<char> format)
             {
-                return CreateNullableFormatter<System.Int16>();
+                return format.Length == 0 ? FastNumberWriter.TryWriteUInt64(dest, out written, x) : x.TryFormat(dest, out written, format);
             }
-            if (type == typeof(System.Int32?))
+            FormatterCache<System.UInt16>.TryFormatDelegate = &UInt16Func;
+
+            [MethodImpl(MethodImplOptions.AggressiveInlining)]
+            static bool UInt32Func(System.UInt32 x, Span<char> dest, out int written, ReadOnlySpan<char> format)
             {
-                return CreateNullableFormatter<System.Int32>();
+                return format.Length == 0 ? FastNumberWriter.TryWriteUInt64(dest, out written, x) : x.TryFormat(dest, out written, format);
             }
-            if (type == typeof(System.Int64?))
+            FormatterCache<System.UInt32>.TryFormatDelegate = &UInt32Func;
+
+            [MethodImpl(MethodImplOptions.AggressiveInlining)]
+            static bool UInt64Func(System.UInt64 x, Span<char> dest, out int written, ReadOnlySpan<char> format)
             {
-                return CreateNullableFormatter<System.Int64>();
+                return format.Length == 0 ? FastNumberWriter.TryWriteUInt64(dest, out written, x) : x.TryFormat(dest, out written, format);
             }
-            if (type == typeof(System.SByte?))
+            FormatterCache<System.UInt64>.TryFormatDelegate = &UInt64Func;
+
+            [MethodImpl(MethodImplOptions.AggressiveInlining)]
+            static bool SingleFunc(System.Single x, Span<char> dest, out int written, ReadOnlySpan<char> format)
             {
-                return CreateNullableFormatter<System.SByte>();
+                return x.TryFormat(dest, out written, format);
             }
-            if (type == typeof(System.Single?))
+            FormatterCache<System.Single>.TryFormatDelegate = &SingleFunc;
+
+            [MethodImpl(MethodImplOptions.AggressiveInlining)]
+            static bool DoubleFunc(System.Double x, Span<char> dest, out int written, ReadOnlySpan<char> format)
             {
-                return CreateNullableFormatter<System.Single>();
+                return x.TryFormat(dest, out written, format);
             }
-            if (type == typeof(System.TimeSpan?))
+            FormatterCache<System.Double>.TryFormatDelegate = &DoubleFunc;
+
+            [MethodImpl(MethodImplOptions.AggressiveInlining)]
+            static bool TimeSpanFunc(System.TimeSpan x, Span<char> dest, out int written, ReadOnlySpan<char> format)
             {
-                return CreateNullableFormatter<System.TimeSpan>();
+                return x.TryFormat(dest, out written, format);
             }
-            if (type == typeof(System.UInt16?))
+            FormatterCache<System.TimeSpan>.TryFormatDelegate = &TimeSpanFunc;
+
+            [MethodImpl(MethodImplOptions.AggressiveInlining)]
+            static bool DateTimeFunc(System.DateTime x, Span<char> dest, out int written, ReadOnlySpan<char> format)
             {
-                return CreateNullableFormatter<System.UInt16>();
+                return x.TryFormat(dest, out written, format);
             }
-            if (type == typeof(System.UInt32?))
+            FormatterCache<System.DateTime>.TryFormatDelegate = &DateTimeFunc;
+
+            [MethodImpl(MethodImplOptions.AggressiveInlining)]
+            static bool DateTimeOffsetFunc(System.DateTimeOffset x, Span<char> dest, out int written, ReadOnlySpan<char> format)
             {
-                return CreateNullableFormatter<System.UInt32>();
+                return x.TryFormat(dest, out written, format);
             }
-            if (type == typeof(System.UInt64?))
+            FormatterCache<System.DateTimeOffset>.TryFormatDelegate = &DateTimeOffsetFunc;
+
+            [MethodImpl(MethodImplOptions.AggressiveInlining)]
+            static bool DecimalFunc(System.Decimal x, Span<char> dest, out int written, ReadOnlySpan<char> format)
             {
-                return CreateNullableFormatter<System.UInt64>();
+                return x.TryFormat(dest, out written, format);
             }
-            if (type == typeof(System.Guid?))
+            FormatterCache<System.Decimal>.TryFormatDelegate = &DecimalFunc;
+
+            [MethodImpl(MethodImplOptions.AggressiveInlining)]
+            static bool GuidFunc(System.Guid x, Span<char> dest, out int written, ReadOnlySpan<char> format)
             {
-                return CreateNullableFormatter<System.Guid>();
+                return x.TryFormat(dest, out written, format);
             }
-            if (type == typeof(System.IntPtr))
+            FormatterCache<System.Guid>.TryFormatDelegate = &GuidFunc;
+
+            FormatterCache<System.Byte?>.TryFormatDelegate = &NullableFormat;
+
+            FormatterCache<System.DateTime?>.TryFormatDelegate = &NullableFormat;
+
+            FormatterCache<System.DateTimeOffset?>.TryFormatDelegate = &NullableFormat;
+
+            FormatterCache<System.Decimal?>.TryFormatDelegate = &NullableFormat;
+
+            FormatterCache<System.Double?>.TryFormatDelegate = &NullableFormat;
+
+            FormatterCache<System.Int16?>.TryFormatDelegate = &NullableFormat;
+
+            FormatterCache<System.Int32?>.TryFormatDelegate = &NullableFormat;
+
+            FormatterCache<System.Int64?>.TryFormatDelegate = &NullableFormat;
+
+            FormatterCache<System.SByte?>.TryFormatDelegate = &NullableFormat;
+
+            FormatterCache<System.Single?>.TryFormatDelegate = &NullableFormat;
+
+            FormatterCache<System.TimeSpan?>.TryFormatDelegate = &NullableFormat;
+
+            FormatterCache<System.UInt16?>.TryFormatDelegate = &NullableFormat;
+
+            FormatterCache<System.UInt32?>.TryFormatDelegate = &NullableFormat;
+
+            FormatterCache<System.UInt64?>.TryFormatDelegate = &NullableFormat;
+
+            FormatterCache<System.Guid?>.TryFormatDelegate = &NullableFormat;
+
+            [MethodImpl(MethodImplOptions.AggressiveInlining)]
+            static bool IntPtrFunc(System.IntPtr x, Span<char> dest, out int written, ReadOnlySpan<char> format)
             {
                 // ignore format
-                return new TryFormat<System.IntPtr>((System.IntPtr x, Span<char> dest, out int written, ReadOnlySpan<char> _) => System.IntPtr.Size == 4
+                return  System.IntPtr.Size == 4
                     ? x.ToInt32().TryFormat(dest, out written, default)
-                    : x.ToInt64().TryFormat(dest, out written, default));
+                    : x.ToInt64().TryFormat(dest, out written, default);
             }
-            if (type == typeof(System.UIntPtr))
+            FormatterCache<System.IntPtr>.TryFormatDelegate = &IntPtrFunc;
+
+            [MethodImpl(MethodImplOptions.AggressiveInlining)]
+            static bool UIntPtrFunc(System.UIntPtr x, Span<char> dest, out int written, ReadOnlySpan<char> format)
             {
                 // ignore format
-                return new TryFormat<System.UIntPtr>((System.UIntPtr x, Span<char> dest, out int written, ReadOnlySpan<char> _) => System.UIntPtr.Size == 4
+                return  System.UIntPtr.Size == 4
                     ? x.ToUInt32().TryFormat(dest, out written, default)
-                    : x.ToUInt64().TryFormat(dest, out written, default));
+                    : x.ToUInt64().TryFormat(dest, out written, default);
             }
-            return null;
+            FormatterCache<System.UIntPtr>.TryFormatDelegate = &UIntPtrFunc;
+
         }
     }
 }
\ No newline at end of file
diff --git a/src/ZString/Utf16/Utf16ValueStringBuilder.CreateFormatter.tt b/src/ZString/Utf16/Utf16ValueStringBuilder.CreateFormatter.tt
index 0ccd705..3684c0f 100644
--- a/src/ZString/Utf16/Utf16ValueStringBuilder.CreateFormatter.tt
+++ b/src/ZString/Utf16/Utf16ValueStringBuilder.CreateFormatter.tt
@@ -32,47 +32,57 @@
     };
 #>
 using System;
+using System.Runtime.CompilerServices;

 namespace Cysharp.Text
 {
     public partial struct Utf16ValueStringBuilder
     {
-        static object CreateFormatter(Type type)
+        static unsafe void RegisterPrimitives()
         {
 <# foreach(var t in spanFormattablesA) { #>
-            if (type == typeof(<#= t.FullName #>))
+            [MethodImpl(MethodImplOptions.AggressiveInlining)]
+            static bool <#= t.Name #>Func(<#= t.FullName #> x, Span<char> dest, out int written, ReadOnlySpan<char> format)
             {
-                return new TryFormat<<#= t.FullName #>>((<#= t.FullName #> x, Span<char> dest, out int written, ReadOnlySpan<char> format) => format.Length == 0 ? FastNumberWriter.TryWriteInt64(dest, out written, x) : x.TryFormat(dest, out written, format));
+                return format.Length == 0 ? FastNumberWriter.TryWriteInt64(dest, out written, x) : x.TryFormat(dest, out written, format);
             }
+            FormatterCache<<#= t.FullName #>>.TryFormatDelegate = &<#= t.Name #>Func;
+
 <# } #>
 <# foreach(var t in spanFormattablesB) { #>
-            if (type == typeof(<#= t.FullName #>))
+            [MethodImpl(MethodImplOptions.AggressiveInlining)]
+            static bool <#= t.Name #>Func(<#= t.FullName #> x, Span<char> dest, out int written, ReadOnlySpan<char> format)
             {
-                return new TryFormat<<#= t.FullName #>>((<#= t.FullName #> x, Span<char> dest, out int written, ReadOnlySpan<char> format) => format.Length == 0 ? FastNumberWriter.TryWriteUInt64(dest, out written, x) : x.TryFormat(dest, out written, format));
+                return format.Length == 0 ? FastNumberWriter.TryWriteUInt64(dest, out written, x) : x.TryFormat(dest, out written, format);
             }
+            FormatterCache<<#= t.FullName #>>.TryFormatDelegate = &<#= t.Name #>Func;
+
 <# } #>
 <# foreach(var t in spanFormattablesC) { #>
-            if (type == typeof(<#= t.FullName #>))
+            [MethodImpl(MethodImplOptions.AggressiveInlining)]
+            static bool <#= t.Name #>Func(<#= t.FullName #> x, Span<char> dest, out int written, ReadOnlySpan<char> format)
             {
-                return new TryFormat<<#= t.FullName #>>((<#= t.FullName #> x, Span<char> dest, out int written, ReadOnlySpan<char> format) => x.TryFormat(dest, out written, format));
+                return x.TryFormat(dest, out written, format);
             }
+            FormatterCache<<#= t.FullName #>>.TryFormatDelegate = &<#= t.Name #>Func;
+
 <# } #>
 <# foreach(var t in spanFormattables) { #>
-            if (type == typeof(<#= t.FullName #>?))
-            {
-                return CreateNullableFormatter<<#= t.FullName #>>();
-            }
+            FormatterCache<<#= t.FullName #>?>.TryFormatDelegate = &NullableFormat;
+
 <# } #>
-<# foreach(var t in new [] {typeof(IntPtr), typeof(UIntPtr)}) { var u = t == typeof(UIntPtr);  #>
-            if (type == typeof(<#= t.FullName #>))
+<# foreach(var t in new [] {typeof(IntPtr), typeof(UIntPtr)} ) { var u = t == typeof(UIntPtr);  #>
+            [MethodImpl(MethodImplOptions.AggressiveInlining)]
+            static bool <#= t.Name #>Func(<#= t.FullName #> x, Span<char> dest, out int written, ReadOnlySpan<char> format)
             {
                 // ignore format
-                return new TryFormat<<#= t.FullName #>>((<#= t.FullName #> x, Span<char> dest, out int written, ReadOnlySpan<char> _) => <#= t #>.Size == 4
+                return  <#= t #>.Size == 4
                     ? x.To<#= u ? "U" : "" #>Int32().TryFormat(dest, out written, default)
-                    : x.To<#= u ? "U" : "" #>Int64().TryFormat(dest, out written, default));
+                    : x.To<#= u ? "U" : "" #>Int64().TryFormat(dest, out written, default);
             }
+            FormatterCache<<#= t.FullName #>>.TryFormatDelegate = &<#= t.Name #>Func;
+
 <# } #>
-            return null;
         }
     }
 }
\ No newline at end of file
diff --git a/src/ZString/Utf16ValueStringBuilder.cs b/src/ZString/Utf16ValueStringBuilder.cs
index 7a4368a..1b9ad83 100644
--- a/src/ZString/Utf16ValueStringBuilder.cs
+++ b/src/ZString/Utf16ValueStringBuilder.cs
@@ -5,7 +5,7 @@ using System.Runtime.CompilerServices;

 namespace Cysharp.Text
 {
-    public partial struct Utf16ValueStringBuilder : IDisposable, IBufferWriter<char>, IResettableBufferWriter<char>
+    public unsafe partial struct Utf16ValueStringBuilder : IDisposable, IBufferWriter<char>, IResettableBufferWriter<char>
     {
         public delegate bool TryFormat<T>(T value, Span<char> destination, out int charsWritten, ReadOnlySpan<char> format);

@@ -32,6 +32,7 @@ namespace Cysharp.Text
                 newLine2 = newLine[1];
                 crlf = true;
             }
+            RegisterPrimitives();
         }

         [ThreadStatic]
@@ -232,7 +233,7 @@ namespace Cysharp.Text
         }

         /// <summary>Appends the string representation of a specified value to this instance.</summary>
-        public void Append<T>(T value)
+        public unsafe void Append<T>(T value)
         {
             if (!FormatterCache<T>.TryFormatDelegate(value, buffer.AsSpan(index), out var written, default))
             {
@@ -361,7 +362,7 @@ namespace Cysharp.Text
         /// are removed from this builder.
         /// </remarks>
         public void Replace(string oldValue, string newValue) => Replace(oldValue, newValue, 0, Length);
-        
+
         public void Replace(ReadOnlySpan<char> oldValue, ReadOnlySpan<char> newValue) => Replace(oldValue, newValue, 0, Length);

         /// <summary>
@@ -578,58 +579,62 @@ namespace Cysharp.Text
             index += written;
         }

+        static class CustomTryFormat<T>
+        {
+            public static TryFormat<T> formatMethod;
+            public static bool Dispatch(T value, Span<char> dest, out int written, ReadOnlySpan<char> format)
+            {
+                return formatMethod(value, dest, out written, format);
+            }
+        }
+
         /// <summary>
         /// Register custom formatter
         /// </summary>
         public static void RegisterTryFormat<T>(TryFormat<T> formatMethod)
         {
-            FormatterCache<T>.TryFormatDelegate = formatMethod;
+            CustomTryFormat<T>.formatMethod = formatMethod;
+            FormatterCache<T>.TryFormatDelegate = &CustomTryFormat<T>.Dispatch;
         }

-        static TryFormat<T?> CreateNullableFormatter<T>() where T : struct
+        static unsafe bool NullableFormat<T>(T? x, Span<char> dest, out int written, ReadOnlySpan<char> format) where T : struct
         {
-            return new TryFormat<T?>((T? x, Span<char> dest, out int written, ReadOnlySpan<char> format) =>
+            if (x == null)
             {
-                if (x == null)
-                {
-                    written = 0;
-                    return true;
-                }
-                return FormatterCache<T>.TryFormatDelegate(x.Value, dest, out written, format);
-            });
+                written = 0;
+                return true;
+            }
+            return FormatterCache<T>.TryFormatDelegate(x.Value, dest, out written, format);
         }

-        /// <summary>
-        /// Supports the Nullable type for a given struct type.
-        /// </summary>
-        public static void EnableNullableFormat<T>() where T : struct
+        public unsafe static class FormatterCache<T>
         {
-            RegisterTryFormat<T?>(CreateNullableFormatter<T>());
-        }
+            private static unsafe delegate*<T, Span<char>, out int, ReadOnlySpan<char>, bool> _TryFormatDelegate;

-        public static class FormatterCache<T>
-        {
-            public static TryFormat<T> TryFormatDelegate;
-            static FormatterCache()
+            public static delegate*<T , Span<char> , out int , ReadOnlySpan<char> , bool> TryFormatDelegate
             {
-                var formatter = (TryFormat<T>)CreateFormatter(typeof(T));
-                if (formatter == null)
+                get
                 {
-                    if (typeof(T).IsEnum)
+                    if (_TryFormatDelegate == null)
                     {
-                        formatter = new TryFormat<T>(EnumUtil<T>.TryFormatUtf16);
-                    }
-                    else if (typeof(T) == typeof(string))
-                    {
-                        formatter = new TryFormat<T>(TryFormatString);
-                    }
-                    else
-                    {
-                        formatter = new TryFormat<T>(TryFormatDefault);
+                        if (typeof(T).IsEnum)
+                        {
+                            _TryFormatDelegate = &EnumUtil<T>.TryFormatUtf16;
+                        }
+                        else if (typeof(T) == typeof(string))
+                        {
+                            _TryFormatDelegate = &TryFormatString;
+                        }
+                        else
+                        {
+                            _TryFormatDelegate = &TryFormatDefault;
+                        }
                     }
+
+                    return _TryFormatDelegate;
                 }

-                TryFormatDelegate = formatter;
+                set => _TryFormatDelegate = value;
             }

             static bool TryFormatString(T value, Span<char> dest, out int written, ReadOnlySpan<char> format)
diff --git a/src/ZString/ZString.csproj b/src/ZString/ZString.csproj
index eab6ce5..bb765cb 100644
--- a/src/ZString/ZString.csproj
+++ b/src/ZString/ZString.csproj
@@ -18,6 +18,7 @@
         <RepositoryUrl>$(PackageProjectUrl)</RepositoryUrl>
         <RepositoryType>git</RepositoryType>
         <PackageLicenseExpression>MIT</PackageLicenseExpression>
+      <LangVersion>9.0</LangVersion>
     </PropertyGroup>

     <ItemGroup>
@@ -187,6 +188,7 @@
     <!-- Copy files for Unity -->
     <PropertyGroup>
         <DestinationRoot>$(MSBuildProjectDirectory)\..\ZString.Unity\Assets\Scripts\ZString\</DestinationRoot>
+        <AssemblyName>NewZString</AssemblyName>
     </PropertyGroup>
     <ItemGroup>
         <TargetFiles1 Include="$(MSBuildProjectDirectory)\**\*.cs" Exclude="**\bin\**\*.*;**\obj\**\*.*;_InternalVisibleTo.cs" />
-- 
2.27.0.windows.1


  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

UnityのC#からGitを叩く

UnityのC#からGitを叩く

UnityのC#のEditor拡張スクリプトからGitコマンドを叩く機会があったので、忘れないうちにまとめます。

サンプルコマンド

今回、例として、UnityのC#から以下のコマンドを叩いてみます。

git config core.autocrlf

改行コードを自動で変換する機能が有効になっているか確認できるコマンドです。

サンプルコード

以下がサンプルコードです。外部から GetAutocrlf を叩けばコマンドが実行されます。

using System;
using System.Diagnostics;
using System.IO;
using UnityEditor;
using UnityEngine;
using Debug = UnityEngine.Debug;

public class GitCommandPractice
{
    /// <summary>
    /// Gitのautocrlfを確認する。
    /// </summary>
    public void GetAutocrlf()
    {
        // gitのパスを取得する。
        string gitPath = GetGitPath();

        // gitのコマンドを設定する。
        string gitCommand = "config core.autocrlf";

        // コマンドを実行して標準出力を取得する。
        string autocrlf = GetStandardOutputFromProcess(gitPath, gitCommand).Trim();

        Debug.Log(autocrlf);
    }

    /// <summary>
    /// Gitの実行ファイルのパスを取得する。
    /// </summary>
    /// <returns>Gitのパス</returns>
    private string GetGitPath()
    {
        // Macのとき
        if (Application.platform == RuntimePlatform.OSXEditor)
        {
            // パスの候補
            string[] exePaths =
            {
                "/usr/local/bin/git",
                "/usr/bin/git"
            };

            // 存在するパスで最初に見つかったもの
            return exePaths.FirstOrDefault(exePath => File.Exists(exePath));
        }

        // Windowsはこれだけで十分
        return "git";
    }

    /// <summary>
    /// コマンドを実行して標準出力を取得する。
    /// </summary>
    /// <param name="exePath">実行ファイルのパス</param>
    /// <param name="arguments">コマンドライン引数</param>
    /// <returns>標準出力</returns>
    private string GetStandardOutputFromProcess(string exePath, string arguments)
    {
        // プロセスの起動条件を設定する。
        ProcessStartInfo startInfo = new ProcessStartInfo()
        {
            FileName = exePath,
            Arguments = arguments,
            WindowStyle = ProcessWindowStyle.Hidden,
            UseShellExecute = false,
            RedirectStandardOutput = true,
        };

        // プロセスを起動する。
        using (Process process = Process.Start(startInfo))
        {
            // 標準出力を取得する。
            string output = process.StandardOutput.ReadToEnd();

            // プロセスが終了するかタイムアウトするまで待つ。
            process.WaitForExit(TimeoutPeriod);

            return output;
        }
    }
}

解説

C# の Prosess を使って、Gitコマンドを叩き、その標準出力を読み取るという仕組みです。

GetGitPath() では、Gitの実行ファイルのパスを取得しています。

Gitの実行ファイルのパスは、環境変数Pathに登録されていることが一般的だと思うので、単に git と記述してもほとんどの場合、問題ないと思います。

ただし、私のMac環境ではそれではうまく行かなかったので、 /usr/local/bin/gitusr/bin/git の2種類を明示的にハードコーディングし、どちらか見つかった方を実行ファイルとして利用するようにしました。

さいごに

本記事作成にあたり、以下を参考にしました。ありがとうございました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

DIコンテナのつらみを補うミドルウェア "Deptorygen"

C#向けのAnalyzer+CodeFixライブラリを作ってみました。

https://github.com/NumAniCloud/Deptorygen

リポジトリ内にあるマニュアルを基にした紹介をしたいと思います。

対象読者

DIコンテナを使用してある程度大きなソフトウェアを開発したことのある方に向けています。

DeptorygenはC#用のアナライザーなので、C#を使用している方に向けています。

既存のDIコンテナとしてGenericHostを例に挙げているので、GenericHostを知っていると理解がスムーズかもしれません。

Deptorygenとは

Deptorygen(でぷとりじぇん)はC#向けのAnalyzer+CodeFixライブラリです。現在、Nuget経由で入手することができます。

Deptorygenの主な役割は、従来のDIコンテナの弱点を補うことです。従来のDIコンテナとはGenericHostのDIコンテナ機能のようなものを指しています。こういったミドルウェアは、インスタンスの依存関係を解決するために動的な処理を用います。それは時に動的コード生成だったり、リフレクションだったりします。

GenericHostにおける注入の設定の書き方
var services = new ServiceCollection();
services.AddSingleton<IService, ServiceGold>();
services.AddTransient<Client>();

一方で、Deptorygenはいわば静的なDIコンテナであり、コンストラクタインジェクションは本当にコンストラクタにインスタンスを注入するようなnew式としてコード生成されます。

静的コード生成で作られるのはファクトリーパターン的な振る舞いをするクラスで、そのクラスの機能はDeptorygenが「ファクトリー定義」と読んでいるような形式のインターフェースとして書かれます。

Deptorygenにおける注入の設定の書き方
[Factory]
interface IFactory
{
    [Resolution(typeof(ServiceGold))]
    IService ResolveService();
    Client ResolveClient();
}

依存関係を静的に解決するDeptorygenは万能ではなく、コンパイル時に型の判明しているものにしか使えません。かといって、依存関係を動的に解決する方法は時に強力すぎで、それを制御するコストが高くつくこともあります。Deptorygenと動的なDIコンテナを組み合わせて使うことがベストな使い方であると考えています。

静的コード生成の利点

不透明さの解消

動的に依存関係を判断してインスタンスを生成する場合、その手順は実行時に決まります。こうなると、プログラマーはインスタンスが実際にどのような手順で生成されているのかを知ることができません。

// この2つのクラス Service, Client の生成をDIで行いたい
class Service {}
class Client
{
    public Client(Service service){}
}

class Program
{
    public static void Main()
    {
        // 生成したい型の情報を登録する
        var services = new ServiceCollection();
        services.AddSingleton<Service>();
        services.AddTransient<Client>();
        var provider = services.BuildServiceProvider();

        // 利用側
        var client = provider.GetService<Client>();

        // →それで、どんな手順でインスタンスを生成しているのだろう?
        // 動的に依存関係が解決されるので分からない
    }
}

Deptorygenでは、インスタンスを生成するコードが静的にコード生成されるため、そのコードを見ればどのような手順で生成されているのかを理解することができます。

ファクトリーを生成してみよう

以下はユーザーの書くコードです。

using System;
using Deptorygen.Annotations;
using UseDeptorygen.Infra;

namespace UseDeptorygen.Samples.BasicDependency
{
    // newしたいクラスたち
    class Service
    {
        public void Show()
        {
            Console.WriteLine("This is Service!");
        }
    }

    class Client
    {
        private readonly Service _service;

        public Client(Service service)
        {
            _service = service;
        }

        public void Execute()
        {
            Console.WriteLine("# Client");
            _service.Show();
        }
    }

    // インターフェースに Factory 属性をつけたものが「ファクトリー定義」
    [Factory]
    interface IFactory
    {
        Service ResolveService();
        Client ResolveClient();
    }

    class Program
    {
        public static void Main()
        {
            var factory = new Factory();
            factory.ResolveClient().Execute();
        }
    }
}

IFactoryの部分にVisual Studioからクイックアクションが提供され、Deptorygenによるコード生成コマンドを実行することができます。

以下は生成されるコードです。

// <autogenerated />
#nullable enable
using System;
using System.Collections.Generic;

namespace UseDeptorygen.Samples.BasicDependency
{
    internal partial class Factory : IFactory
        , IDisposable
    {
        private Service? _ResolveServiceCache;
        private Client? _ResolveClientCache;

        public Factory()
        {
        }

        public Service ResolveService()
        {
            return _ResolveServiceCache ??= new Service();
        }

        public Client ResolveClient()
        {
            return _ResolveClientCache ??= new Client(ResolveService());
        }

        public void Dispose()
        {
        }
    }
}

生成されたFactoryクラスではいくつかの機能をサポートしています。

  • ServiceクラスをnewするResolveServiceメソッド
  • ClientクラスをnewするResolveClientメソッド
  • newしたService,Clientインスタンスはキャッシュする
  • キャッシュしたインスタンスがIDisposableなら、それをまとめてDisposeできる機能
  • ファクトリー定義として使用したインターフェースを実装している
  • 必要な名前空間があればusingする

ガイド:基本的な使い方
↑話題に関連するマニュアルのページへのリンクを張っておきますので参考にしてください。

無効な設定にコンパイルエラーを出す

動的に依存関係を判断してインスタンスを生成する場合、依存関係を解決する手段が実行時に決まるということなので、実際には依存関係を解決できないような設定でDIコンテナが使用されている場合にコンパイルエラーを出すことができません。

ここからはMainメソッドを省略した簡易的なコードで紹介します。トップレベルに処理が書かれていたら、それはMainメソッドの内部です。

class Service { }

var services = new ServiceCollection();

// DIコンテナに Service を登録するのを忘れちゃった
// services.AddSingleton<Service>();
var provider = services.BuildServiceProvider();

// 利用側
// ここが実行時エラーになる(コンパイルエラーにならない)
var service = provider.GetService<Service>();

Deptorygenを用いてファクトリークラスを生成すると、依存関係の解決が不可能であった型に対しては無効なコードが生成され、コンパイルエラーとなります。

class Service { }

[Factory]
interface IFactory
{
    // Service 型に対するメソッドを書き忘れちゃった
}

// こんな感じのファクトリーが生成される(機能がからっぽ)
public class Factory : IFactory, IDisposable
{
    public void Dispose()
    {
    }
}

// 利用側
var factory = new Factory();
// ResolveServiceなるメソッドは存在しないのでコンパイルエラーが出る
var service = factory.ResolveService();

ただし、依存先のインスタンスを生成することができないことにより依存関係の解決が不可能であった場合はファクトリークラス自体は有効なコードが生成されます。

その代わり、足りない依存先がコンストラクタの引数でもって利用者に対して要求されます。この引数にインスタンスを渡したくない場合はファクトリーも生成できないことになるため、プログラマーはファクトリークラスに対して十分に型の情報を伝える必要があることに気づくことができます。

class Service { }
class Client
{
    public Client(Service service) { }
}

[Factory]
interface IFactory
{
    // Service 型に対するメソッドを書き忘れちゃった
//  Service ResolveService();
    Client ResolveClientAsTransient();
}

// 生成されるコード
internal class Factory : IFactory, IDisposable
{
    private readonly Service _service;

    // 足りない依存先はコンストラクタで外部に要求する
    public Factory(Service service)
    {
        _service = service;
    }

    public Client ResolveClientAsTransient()
    {
        // Service に対するメソッドが無いので、仕方なくフィールドに持ってるインスタンスを使う
        // フィールドの中身は、コンストラクタを通じて渡される前提
        return new Client(_service);
    }

    public void Dispose()
    {
    }
}

ガイド:コンストラクタで意外な引数を要求されたら

追加の引数を与える

動的に依存関係を判断してインスタンスを生成するDIコンテナでは、実際にインスタンスを生成するタイミングで初めて得られるような情報を追加で引数に渡して、適切に設定されたインスタンスを生成できるものもあります。しかし、こうして与える追加の引数についてコンパイル時に型チェックをしてもらうことは困難です。

// これは今からnewしたいクラス
class Service
{
    public Service(string message) { }
}

var services = new ServiceCollection();
services.AddSingleton<Service>();   // ここでは引数に関する情報を伝えない

var provider = services.BuildServiceProvider();

// 生成時に引数を渡せる。ただしprovider.GetService<Service>() という書き方はできない
// 引数が (ServiceProvider, params object[]) なのでIntellisenseも効かない
var instance = ActivatorUtilities.CreateInstance<Service>(provider, "SomethingMessage");

// 型チェックがないので、stringを渡すべき場所に何でも渡せてしまう
// これは実行時エラーになる
var invalid = ActivatorUtilities.CreateInstance<Service>(provider, DateTime.Now);

DeptorygenでもそうしたDIコンテナと同様に、インスタンスを生成するときに追加の引数を渡すことができます。ただし、依存関係を解決するコードは静的に生成されているため、追加で渡す引数も必ず型チェックの対象となります。

// これは今からnewしたいクラス
class Service
{
    public Service(string message) { }
}

[Factory]
interface IFactory
{
    // ファクトリー定義の時点で引数の情報を伝えておく
    Service ResolveService(string message);
}

// 生成されるクラスは以下のような感じ
internal partial class Factory : IFactory
    , IDisposable
{
    private Service? _ResolveServiceCache;

    public Factory()
    {
    }

    public Service ResolveService(String message)
    {
        return _ResolveServiceCache ??= new Service(message);
    }

    public void Dispose()
    {
    }
}

var factory = new Factory();

// 生成時に引数を渡せる。静的コード生成なのでIntellisenseも効く
var instance = factory.ResolveService("SomethingMessage");

// 型チェックが効くので、これはコンパイルエラーになる
var invalid = factory.ResolveService(DateTime.Now);

サンプル:解決メソッドに直接オブジェクトを渡す

インスタンスの自由な寿命管理

DIコンテナにおいてインスタンスの寿命を直感的に管理するのは難しい課題です。筆者の利用したものの多くは、インスタンスの寿命はSingleton, Scope, Transient といった3つ程度の区分に分かれ、あとはDIコンテナ独自のクラス構造を駆使してスコープや寿命を管理します。

例えばGenericHostのDIコンテナであれば、ServiceProviderのインスタンス1つが1つのスコープに対応しています。

Deptorygenでは、インスタンスの寿命はそのインスタンスをキャッシュしているファクトリーが基準となります。ファクトリークラスはstaticなものではないし、DIコンテナとしての特別な機能が備わっているクラスでもないので、依存関係を注入する対象のクラスたちと同様に取りまわすことができます。もちろん、ファクトリーがファクトリーを生成することも可能です。

Deptorygenでのインスタンスの寿命は2種類です。Cached……つまりファクトリーそのものと同じ寿命か、Transient……生成するたびに違うインスタンスか、です。

ファクトリー自体をシングルトンにするのも自由です。その場合、寿命がCachedであるインスタンスもシングルトンな寿命を持つことになります。ファクトリーのコンストラクタがprivateになることをファクトリー定義で指示することが現状ではできないので、シングルトンにするには別のクラスに包含させる、あるいはファクトリー自体をDIコンテナに生成させるなどの工夫は要りそうです。

他にも、ファクトリークラスを生成する種となる複数のインターフェース定義のあいだに継承や包含の関係を持たせれば、複数のファクトリー間でキャッシュを共有したり、特定のインスタンスを生成する権利を持つクラスを限定するなどの使い方ができたりなど、DIコンテナを使わない場合と同じくらいに寿命とスコープを柔軟に管理することができます。

ガイド:ファクトリーを別のアセンブリに提供する

サンプル:依存関係の解決に別のファクトリーも利用する(キャプチャ)

動的な依存解決との組み合わせ

プラグインで拡張のできるアプリなどを開発していると、外部からどのようなクラスが供給されるのか不明な場合があります。

特に、2つのプラグイン間で依存関係が存在する場合は困難な問題になります。どのようなクラスが供給されるのかだけでなく、どのようなクラスが要求されるのかすら不明なため、静的に依存関係を解決できる可能性は絶望的です。

こうなった場合、動的な依存解決の出番です。

Deptorygenは現在GenericHostのDIコンテナと連携する機能があり、Deptorygenの生成したファクトリークラスをGenericHostが依存解決する際に利用するよう登録できます。

以下はユーザーの書くコードです。

// newしたいクラス Service, Service2, Client
class Service { }

class Service2 { }

class Client
{
    public Client(Service service, Service2 service2) { }
    public void Work() { /* service, service2 を使って何かする */ }
}

// ConfigureGenericHost 属性をつけると、GenericHostで使えるようになる
[Factory]
[ConfigureGenericHost]
interface IFactory
{
    Service ResolveService();
    Service2 ResolveService2();
    Client ResolveClient();
}

class GenericHostSample
{
    public void Run()
    {
        var services = new ServiceCollection();

        // GenericHost の ServiceCollection インスタンスに、ファクトリーのインスタンスを登録する
        services.UseDeptorygenFactory(new Factory());

        var serviceProvider = services.BuildServiceProvider();

        // Factory クラスで解決できる依存関係が、ServiceProvider からも解決できるようになる
        serviceProvider.GetService<Client>().Work();
    }
}

以下のようなコードが生成されます。

// <autogenerated />
#nullable enable
using System;
using System.Collections.Generic;
using Deptorygen.GenericHost;
using Microsoft.Extensions.DependencyInjection;

namespace UseDeptorygen.Samples.GenericHost
{
    internal partial class Factory : IFactory
        , IDisposable
        , IDeptorygenFactory
    {
        private Service? _ResolveServiceCache;
        private Service2? _ResolveService2Cache;
        private Client? _ResolveClientCache;

        public Factory()
        {
        }

        public Service ResolveService()
        {
            return _ResolveServiceCache ??= new Service();
        }

        public Service2 ResolveService2()
        {
            return _ResolveService2Cache ??= new Service2();
        }

        public Client ResolveClient()
        {
            return _ResolveClientCache ??= new Client(ResolveService(), ResolveService2());
        }

        // GenericHostと連携するためのメソッド
        public void ConfigureServices(IServiceCollection services)
        {
            // キャッシュはファクトリー側が管理するので、すべてTransient
            services.AddTransient<IFactory>(provider => this);
            services.AddTransient<Service>(provider => ResolveService());
            services.AddTransient<Service2>(provider => ResolveService2());
            services.AddTransient<Client>(provider => ResolveClient());
        }

        public void Dispose()
        {
        }
    }
}

この Factory クラスは IDeptorygenFactoryを実装しています。クラスがIDeptorygenFactory を実装していると、UseDeptorygenFactory拡張メソッドに渡すことができます。

ConfigureServicesメソッドがIDeptorygenFactoryの実装に必要なAPIです。

サンプル:GenericHostと連携する

まとめ

Deptorygenのコンセプトは、「静的に解決できる部分だけでも静的に解決する」です。依存関係を静的に解決することで生まれるいかなる潜在能力にぼくが期待しているかは、この記事には書ききれません。もういくつか紹介の記事を書くかもしれませんが、おそらくリポジトリに用意したマニュアルと同程度の紹介になると思います。

興味のある方はDeptorygenを使ってみてください。Twitterなどで感想・要望をもらえると嬉しいです。GitHubのissueを通じて要望を受け付ける予定はありませんが、issueを立ててもらってもそれほど困らないのでどうぞ。

Deptorygenにはまだ細かい問題が残っており、使いづらいこともあるかもしれません。反響があればディスカッションの類はSlackを立てて、そこでしたいかなと思っています。

それと……依存関係を静的に解決するというアイデアは別の言語にも適用できるだろうし、Deptorygenとは違った実装をC#に与えることもできると考えています。同じアイデアのミドルウェアがあれば教えてください。そして、皆さんもこのようなミドルウェアを作ってみると面白い挑戦になるかもしれません。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

uGUIにおけるポインターイベントの伝播 (Unity)

前提

環境

  • Unity 2019.4.6 (LTS)

目的

  • uGUIにおける主なポインターイベントのバブリングを研究します。

公式ドキュメント

イベントの伝播を確認する

準備

  • ▼♻Scene
    • ▼?Canvas
      • ▼?Obj (0)
        • ▼?Obj (1)
          • ?Obj (2)
    • ?EventSystem

Obj (0~2)には、イベントを受け取るImageと、受け取ったイベントとhoveredを報告するCheckPointerEventをアタッチ

CheckPointerEventの内容
CheckPointerEvent.cs
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UnityEngine.EventSystems;

public class CheckPointerEvent : MonoBehaviour
    , IBeginDragHandler, IDragHandler, IEndDragHandler, IDropHandler  // ドラッグ
    , IPointerEnterHandler, IPointerExitHandler // 領域出入
    , IPointerUpHandler, IPointerDownHandler, IPointerClickHandler // クリック
{

    public void OnBeginDrag (PointerEventData eventData) {
        Debug.Log ($"{name}.OnBeginDrag [{string.Join (", ", eventData.hovered.ConvertAll (o => o.name))}]");
    }

    public void OnEndDrag (PointerEventData eventData) {
        Debug.Log ($"{name}.OnEndDrag [{string.Join (", ", eventData.hovered.ConvertAll (o => o.name))}]");
    }

    public void OnDrop (PointerEventData eventData) {
        Debug.Log ($"{name}.OnDrop [{string.Join (", ", eventData.hovered.ConvertAll (o => o.name))}]");
    }

    private List<GameObject> lastDragHoverd; // 最後のドラッグ・ログ
    public void OnDrag (PointerEventData eventData) {
        if (lastDragHoverd == null || !lastDragHoverd.SequenceEqual (eventData.hovered)) { // 異なるときだけ
            Debug.Log ($"{name}.OnDrag [{string.Join (", ", eventData.hovered.ConvertAll (o => o.name))}]");
            lastDragHoverd = new List<GameObject> (eventData.hovered);
        }
    }

    public void OnPointerEnter (PointerEventData eventData) {
        Debug.Log ($"{name}.OnPointerEnter [{string.Join (", ", eventData.hovered.ConvertAll (o => o.name))}]");
    }

    public void OnPointerExit (PointerEventData eventData) {
        Debug.Log ($"{name}.OnPointerExit [{string.Join (", ", eventData.hovered.ConvertAll (o => o.name))}]");
    }

    public void OnPointerUp (PointerEventData eventData) {
        Debug.Log ($"{name}.OnPointerUp [{string.Join (", ", eventData.hovered.ConvertAll (o => o.name))}]");
    }

    public void OnPointerDown (PointerEventData eventData) {
        Debug.Log ($"{name}.OnPointerDown [{string.Join(", ", eventData.hovered.ConvertAll (o => o.name))}]");
    }

    public void OnPointerClick (PointerEventData eventData) {
        Debug.Log ($"{name}.OnPointerClick [{string.Join (", ", eventData.hovered.ConvertAll (o => o.name))}]");
    }

}

試行と結果

親「エンター、ダウン、ドラッグ、アップ、イグジット」
Obj (0).OnPointerEnter []
Obj (0).OnPointerDown [Obj (0), Canvas]
Obj (0).OnBeginDrag [Obj (0), Canvas]
Obj (0).OnPointerUp [Obj (0), Canvas]
Obj (0).OnPointerClick [Obj (0), Canvas]
Obj (0).OnEndDrag [Obj (0), Canvas]
Obj (0).OnPointerExit [Obj (0), Canvas]
親「エンター、ダウン、ドラッグ、イグジット、ドラッグ、アップ」
Obj (0).OnPointerEnter []
Obj (0).OnPointerDown [Obj (0), Canvas]
Obj (0).OnBeginDrag [Obj (0), Canvas]
Obj (0).OnDrag [Obj (0), Canvas]
Obj (0).OnPointerExit [Obj (0), Canvas]
Obj (0).OnDrag []
Obj (0).OnPointerUp []
子「エンター、ダウン、ドラッグ、アップ、イグジット」
Obj (1).OnPointerEnter []
Obj (0).OnPointerEnter [Obj (1)]
Obj (1).OnPointerDown [Obj (1), Obj (0), Canvas]
Obj (1).OnBeginDrag [Obj (1), Obj (0), Canvas]
Obj (1).OnDrag [Obj (1), Obj (0), Canvas]
Obj (1).OnPointerUp [Obj (1), Obj (0), Canvas]
Obj (1).OnPointerClick [Obj (1), Obj (0), Canvas]
Obj (1).OnEndDrag [Obj (1), Obj (0), Canvas]
Obj (1).OnPointerExit [Obj (1), Obj (0), Canvas]
Obj (0).OnPointerExit [Obj (1), Obj (0), Canvas]
子「エンター、ダウン、ドラッグ、イグジット、ドラッグ、アップ」
Obj (1).OnPointerEnter []
Obj (0).OnPointerEnter [Obj (1)]
Obj (1).OnPointerDown [Obj (1), Obj (0), Canvas]
Obj (1).OnBeginDrag [Obj (1), Obj (0), Canvas]
Obj (1).OnDrag [Obj (1), Obj (0), Canvas]
Obj (1).OnPointerExit [Obj (1), Obj (0), Canvas]
Obj (0).OnPointerExit [Obj (1), Obj (0), Canvas]
Obj (1).OnDrag []
Obj (1).OnPointerUp []
Obj (1).OnEndDrag []
孫「エンター、ダウン、ドラッグ、アップ、イグジット」
Obj (2).OnPointerEnter []
Obj (1).OnPointerEnter [Obj (2)]
Obj (0).OnPointerEnter [Obj (2), Obj (1)]
Obj (2).OnPointerDown [Obj (2), Obj (1), Obj (0), Canvas]
Obj (2).OnBeginDrag [Obj (2), Obj (1), Obj (0), Canvas]
Obj (2).OnDrag [Obj (2), Obj (1), Obj (0), Canvas]
Obj (2).OnPointerUp [Obj (2), Obj (1), Obj (0), Canvas]
Obj (2).OnPointerClick [Obj (2), Obj (1), Obj (0), Canvas]
Obj (2).OnEndDrag [Obj (2), Obj (1), Obj (0), Canvas]
Obj (2).OnPointerExit [Obj (2), Obj (1), Obj (0), Canvas]
Obj (1).OnPointerExit [Obj (2), Obj (1), Obj (0), Canvas]
Obj (0).OnPointerExit [Obj (2), Obj (1), Obj (0), Canvas]
孫「エンター、ダウン、ドラッグ、イグジット、ドラッグ、アップ」
Obj (2).OnPointerEnter []
Obj (1).OnPointerEnter [Obj (2)]
Obj (0).OnPointerEnter [Obj (2), Obj (1)]
Obj (2).OnPointerDown [Obj (2), Obj (1), Obj (0), Canvas]
Obj (2).OnBeginDrag [Obj (2), Obj (1), Obj (0), Canvas]
Obj (2).OnPointerExit [Obj (2), Obj (1), Obj (0), Canvas]
Obj (1).OnPointerExit [Obj (2), Obj (1), Obj (0), Canvas]
Obj (0).OnPointerExit [Obj (2), Obj (1), Obj (0), Canvas]
Obj (2).OnDrag []
Obj (2).OnPointerUp []
Obj (2).OnEndDrag []
  • 他に、ドラッグして他のオブジェクトにドロップすると、落とされたオブジェクトにのみOnDropが届く様子を確認できました。

考察

結果の解釈

  • OnPointerEnterOnPointerExitはターゲット以降もバブルアップ(親へ伝播)されます。
    • その他(クリック、ドラッグ)はターゲットで留められてバブルアップされません。
    • このイベントによる違いは、ボタンを親子に配置することでも確認できます。
      • 子ボタンをポインターで指すと、親ボタンもHighlighted状態になりますが、クリックしても親ボタンには伝わりません。
  • hoveredは、「マウスが上にのっているオブジェクトのスタックリスト」と説明されていますが、イベント発生直前の親を辿るようです。
    • 特に、OnPointerEnterでは侵入直前の状態を示すため、ポインターが孫に侵入した場合、孫では侵入前なので何もなし、バブルアップされた子では孫に侵入済み、さらにバブルアップされた親では孫と子に侵入済みとなるようです。
    • 他のイベントの多くは、直前が既にオブジェクトに乗った状態なので、総親のCanvasまで辿られています。
    • 対して、オブジェクトから外れてからドラッグを終えると、何にも乗っていないことになります。

疑問

  • イベントによって、ターゲットがイベントを受け取った後のバブルアップの有無が異なるようですが、どのようなルールなのでしょうか?
    • 固定的なものなのか、設定あるいは制御可能なものなのでしょうか?
  • いろいろ調べて見ましたが、関係するような記述は発見できませんでした。
    • 制御できても良さそうなものですが、「そういうもの」として設計されているということでしょうか。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

標準入力値のスペース区切りの方法

文字列の場合

string[] array = Console.ReadLine().Split(' ');
string m = array[0];
string n = array[1];

数字の場合

string[] array = Console.ReadLine().Split(' ');
int m = int.Parse(array[0]);
int n = int.Parse(array[1]);
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む