ほげほげー

C#メインにプログラミング周りから日常のあれこれとかを不定期に書いていきます。

JsonConverter<T> を使って多態化されたオブジェクトを JsonSerializer でシリアライズ・デシリアライズする

前の記事に記載した通り、.NET Core 3.1 から追加された JsonSerializer はポリモーフィズムには対応していません。
そのため、JsonConverter<T> を使って自前で対応する必要があります。

この記事では前提として以下の名前空間の宣言とクラスが存在している事とします。

using System.Text.Json;
using System.Text.Json.Serialization;

public abstract class GameMachine
{
    public GameMachine(int price)
    {
        Price = price;
    }
    public int Price { get; init; }
}

public class NintendoSwitch : GameMachine
{
    public NintendoSwitch(bool isLite, int price)
        : base(price)
    {
        IsLite = isLite;
    }
    public bool IsLite { get; init; }
}

public class XboxSeries : GameMachine
{
    public XboxSeries(EditionType edition, int price)
        : base(price)
    {
        Edition = edition;
    }
    public EditionType Edition { get; init; }
    public enum EditionType
    {
        X,
        S
    }
}

public class PlayStation5 : GameMachine
{
    public PlayStation5(bool isDigitalOnly, int price)
        : base(price)
    {
        IsDigitalOnly = isDigitalOnly;
    }
    public bool IsDigitalOnly { get; init; }
}

JsonConvert<T> 無しでの挙動を確認する

基底クラスに触れない形で実装。

public class GameCatalog
{
    public NintendoSwitch NintendoSwitch { get; }
    public XboxSeries Xbox { get; }
    public PlayStation5 PlayStation { get; }
    public GameCatalog(NintendoSwitch nintendoSwitch, XboxSeries xbox, PlayStation5 playStation)
    {
        NintendoSwitch = nintendoSwitch;
        Xbox = xbox;
        PlayStation = playStation;
    }
}

void Main()
{
    var catalog = new GameCatalog(
        new NintendoSwitch(true, 20000),
        new XboxSeries(XboxSeries.EditionType.X, 50000),
        new PlayStation5(false, 60000)
    );
    var json = JsonSerializer.Serialize(catalog);
    var catalog2 = JsonSerializer.Deserialize<GameCatalog>(json);
}

すると、正常にシリアライズ・デシリアライズが出来ている事が分かります。

f:id:tyhe:20210413182303p:plain

続いてこれを基底クラスを使って実装。

public class GameCatalog
{
    public GameMachine[] GameMachines { get; }
    public GameCatalog(params GameMachine[] gameMachines)
    {
        GameMachines = gameMachines;
    }
}

void Main()
{
    var catalog = new GameCatalog(
        new NintendoSwitch(true, 20000),
        new XboxSeries(XboxSeries.EditionType.X, 50000),
        new PlayStation5(false, 60000)
    );
    var json = JsonSerializer.Serialize(catalog);
    var catalog2 = JsonSerializer.Deserialize<GameCatalog>(json);
}

これを実行すると Deserialize 時に以下のエラーが発生します。

Deserialization of types without a parameterless constructor, a singular parameterized constructor, or a parameterized constructor annotated with 'JsonConstructorAttribute' is not supported. Type 'GameMachine'

ポリモーフィズムに対応していないのでこうなるのは正しい挙動です。 一方で、Serialize 時にはエラーが出ておらず、正常に実行されたように見えますが、実際は以下のJSON が出力されており、基底クラスで定義されている物しか出力されていないことが分かります。

{"GameMachines":[{"Price":20000},{"Price":50000},{"Price":60000}]}

それでは JsonConverer<T> を実装してポリモーフィズムに対応してみます。

JsonConverter<T> の実装

JsonConverter<GameMachine> を実装して GameMachine のポリモーフィズムに対応します。

JsonConverter の実装はこちら。コメントで色々と

public class GameMachineConverter : JsonConverter<GameMachine>
{
    /// <summary>
    /// 識別子
    /// </summary>
    private enum Discriminator
    {
        NintendoSwitch,
        XboxSeries,
        PlayStation5
    }
    
    /// <summary>
    /// GameMachine を継承していたら変換可能とする
    /// </summary>
    public override bool CanConvert(Type typeToConvert)
        => typeof(GameMachine).IsAssignableTo(typeToConvert);

    /// <summary>
    /// 抽象メソッドの JsonConverter<T>.Read を実装。
    /// Deserialize 時に呼ばれる。
    /// 識別子を元にインスタンス化するクラスを選択する。
    /// </summary>
    /// <remarks>
    /// コメント上では前提として value が NintendoSwitch であることとする
    /// </remarks>
    public override GameMachine Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        // {"Discriminator":0,"Value":{"Price":21978,"IsLite":true}} を順に読んでいく
        
        // 初期状態では "{" が読み込まれているはず
        if (reader.TokenType != JsonTokenType.StartObject) throw new JsonException("オブジェクト開始文字から始まっていない");

        // 1つ読み進める
        // プロパティ名が読み込まれる: Discriminator
        if (!reader.Read() || reader.TokenType != JsonTokenType.PropertyName || reader.GetString() != "Discriminator") throw new JsonException("Discriminator が見つからない");

        // 1つ読み進める
        // Discriminator プロパティにセットされている値が読み込まれる: 0
        if (!reader.Read() || reader.TokenType != JsonTokenType.Number) throw new JsonException("Discriminator の値が数値ではない");

        // Discriminator プロパティの値を取得する: 0
        var discriminator = (Discriminator)reader.GetInt32();

        // 1つ読み進める
        // プロパティ名が読み込まれる(Value)
        if (!reader.Read() || reader.GetString() != "Value") throw new JsonException("Value が見つからない");

        // 1つ読み進める
        // "Value": の次は { なのでオブジェクト開始文字 "{" が読み込まれる
        if (!reader.Read() || reader.TokenType != JsonTokenType.StartObject) throw new JsonException("オブジェクトが見つからない");

        // Discriminator を元に型を指定してデシリアライズする
        // ここで early return 出来そうだけどしちゃだめで、reader が最後の "}" まで読み込んでから return する必要がある
        var result = (GameMachine)JsonSerializer.Deserialize(ref reader, GetTypeByDiscriminator(discriminator));

        // 1つ読み進めてオブジェクト終了文字まで読み込む
        if (!reader.Read() || reader.TokenType != JsonTokenType.EndObject) throw new JsonException("オブジェクト終了文字が見つからない");

        return result;
    }
    
    /// <summary>
    /// Discriminator から型情報を取得する
    /// </summary>
    private Type GetTypeByDiscriminator(Discriminator discriminator)
    {
        switch (discriminator)
        {
            case Discriminator.NintendoSwitch:
                return typeof(NintendoSwitch);
                
            case Discriminator.XboxSeries:
                return typeof(XboxSeries);
                
            case Discriminator.PlayStation5:
                return typeof(PlayStation5);
                
            default:
                throw new IndexOutOfRangeException("未定義の Discriminator が指定された");
        }
    }

    /// <summary>
    /// 抽象メソッドの JsonConverter<T>.Write を実装。
    /// Serialize 時に呼ばれる。
    /// 派生クラスを識別可能とするために識別子的なものを付与した JSON を出力させる。
    /// JsonSerializerOptions は今回は使わない
    /// </summary>
    /// <remarks>
    /// コメント上では前提として value が NintendoSwitch であることとする
    /// </remarks>
    public override void Write(Utf8JsonWriter writer, GameMachine value, JsonSerializerOptions options)
    {
        // "{" を書き込む
        // {
        writer.WriteStartObject();
        
        // 識別子を書き込む
        // {"Discriminator":0
        writer.WriteNumber("Discriminator", (int)GetDiscriminator(value));
        
        // value を格納するプロパティを書き込む
        // {"Discriminator":0,"Value":
        writer.WritePropertyName("Value");
        
        // value をシリアライズする
        // {"Discriminator":0,"Value":{"Price":21978,"IsLite":true}
        JsonSerializer.Serialize(writer, value, value.GetType());
        
        // "}" を書込
        // {"Discriminator":0,"Value":{"Price":21978,"IsLite":true}}
        writer.WriteEndObject();
    }
    
    /// <summary>
    /// 識別子を取得する
    /// </summary>
    private Discriminator GetDiscriminator(GameMachine value)
    {
        switch (value)
        {
            case NintendoSwitch:
                return Discriminator.NintendoSwitch;
                
            case XboxSeries:
                return Discriminator.XboxSeries;

            case PlayStation5:
                return Discriminator.PlayStation5;
                
            default:
                throw new IndexOutOfRangeException("未定義の型が指定された");
        }
    }
}

これを実装した上で先ほど失敗したコードを実行してみます。

    var catalog = new GameCatalog(
        new NintendoSwitch(true, 20000),
        new XboxSeries(XboxSeries.EditionType.X, 50000),
        new PlayStation5(false, 60000)
    );
    var json = JsonSerializer.Serialize(catalog);
    var catalog2 = JsonSerializer.Deserialize<GameCatalog>(json);

f:id:tyhe:20210415190013p:plain

正常に動作するようになりました。

これくらいのコードなら自動生成もできると思うので必要になったら自動生成を実装すると良さそうです。

System.Text.Json.JsonSerializer のジェネリック版関数について

.NET Core 3.1 から JsonSerializer が入ったので今更ながら試してみようかと思った触ってみた時に遭遇した、ちょっと考えればまぁ分からないでもないけど罠だなぁ…っていう挙動を覚書。

JsonSerializer について

JsonSerializer は .NET Core 3.1 から導入されたシリアライザです。

JsonSerializer にはジェネリック版と非ジェネリック版の Serialize, Deserialize が用意されています。

基本的にはジェネリック版で良いと思いますが、ポリモーフィズムを持ったオブジェクトを扱う際には注意が必要になります。

ちなみにポリモーフィズムを持ったオブジェクトのデシリアライズにはデフォルトでは対応していないので、JsonConverter<T> を継承して独自実装する必要があります。 これは後日。

実際に困る場合の例はこちら。

public class Program
{
    public abstract class Item
    {
        public int Price { get; init; }
        public string Name { get; init; }
    }

    public class Fruit : Item
    {
        public int SugarContent { get; init; }
    }

    public class Meat : Food
    {
        public int FatLevel { get; init; }
    }

    public Main()
    {
    Item item = new Fruit
        {
            Price = 100,
            Name = "りんご",
            SugarContent = 14
        };
        var json = JsonSerializer.Serialize(item);
        // 以下略
    }
}

こうした時に、json の中身には Item が持っているプロパティしか存在せず、情報が欠落してしまいます。

Item 型の変数に入れた状態で Serialize したため <T> が Item になった結果です。

これを回避するためには

  1. 具体的な型の変数に入れる
  2. ジェネリック版の関数を使って Serialize(item, item.GetType()) とする
  3. JsonConverter<Item> を実装して派生クラスの扱いを記述する

の3つが選択肢に上がります。

現実的にはには Item の配列とかが持ちたいケースが大半だったりすると思うので、JsonConverter 一択になるんじゃないかとは思います。

JsonConverter の実装が面倒な場合はシリアライズ、デシリアライズ時に具体的な型を指定するような構造にすることが必要になります。その場合は非ジェネリック版の関数を必ず使う、とかしないと事故るケースが出てきそうです。*1

*1:とはいえ、そんなことは書き捨てのコード以外ではやらないと思いますが

JavaScript における yield の話とかアロー関数の話とか

JavaScript で業務用のブックマークレットでも書くかと思って書いてたら何か自分が知ってる JavaScript からだいぶ変わってたのでメモがてら。

最近の JavaScript について

もはや JavaScript を勉強したのは何年前だって状態なので、間の機能を補完しようかと思ったのですが、何が増えているのかさっぱりわからないのでとりあえず yieldラムダ式 アロー関数 の話でも。async/await もいいんですが、まぁこれは気が向いたら。

yield について

yield は、委譲するとか譲るとかの意味を持った英単語で、実際 yield の使用時には値を実処理側に委譲するような動きをします。

記述方法は以下の形式です。

function* sample() {
  yield "one";
  yield "two";
  yield "three";
}

function の後にアスタリスクを付けることで yield キーワードが使えるようになります。

この関数の戻り値は Generator 型となり、Generator.prototype.next() 関数や for of*1 を使って要素を一つずつ処理することができます。

let values = sample();
console.log(values.next()); // {value: "one", done: false}
console.log(values.next()); // {value: "two", done: false}
console.log(values.next()); // {value: "three", done: false}
console.log(values.next()); // {value: undefined, done: true}

ループで回して値を取得する場合は

let values = sample();
while ((let next = values.next()).done === false) {
  console.log(next.value);
}

または

let values = sample();
for (const value of values) {
  console.log(value);
}

で利用できます。基本は後者ですかね。

これができると何が嬉しいかというと、列挙処理において遅延評価がしやすくなる点と、同じことをしようとした場合に Iterator パターンを一から実装しないといけなかったものが簡潔に書けるようになった点にあります。

C# でも yield return でほぼ同様のことができる実現でき、それについてはこちらに諸々まとめてあります。

Generator 型について

さて、C# を慣れ親しんだ身としては yield で返された値は LINQ っぽく扱いたくなるもので、これは Generatorprototype を拡張してやればできるはずです。

とりあえず拡張して U Select<T, U>(this IEnumerable<T, U> source, Func<T, U> selector) 的なものを足してみましょう。

Generator.prototype.select = function*(seletor){for (item of this) { yield selector(item); }};

すると、Generator が見つからず、エラーになってしまいます。

Generator は直接インスタンスを生成することが許可されていないため、触れる場所にはありません。

では yield で返ってきた Generatorprototype を使ってみましょう。

const generator = (function*(){})();
generator.__proto__.select = function*(seletor){for (item of this) { yield selector(item); }};

できたっぽいので使ってみましょう。

const sample = (function*() {
  yield 1;
  yield 2;
  yield 3;
})();
for (const item of sample.select(i => i * 10)) {
  console.log(item);
}

実行すると以下のエラーが発生します。

VM2202:7 Uncaught TypeError: s.select is not a function or its return value is not iterable

何故でしょうか?

C# で yield return に触れていると何となく原因はこれなのかな?
というものが頭に浮かびます。

yield は結局のところシンタックスシュガーで、Generator は実行エンジン側で動的に生成された型になっているのです。

つまり Generator を生成する関数毎に異なる型が返却されていることになります。

そこでデバッグコンソールからオブジェクトのプロトタイプチェーンを眺めてみると

f:id:tyhe:20210109020603p:plain

どうやら Generator の上にさらに Generator がいました。

こいつがどうやら大元っぽいのでこいつを拡張してみましょう。

const generator = (function*(){})();
generator.__proto__.__proto__.select = function*(seletor){for (item of this) { yield selector(item); }};

で、これを使ってみましょう。

const sample = (function*() {
  yield 1;
  yield 2;
  yield 3;
})();
for (const item of sample.select(i => i * 10)) {
  console.log(item);
}

動きました。

これで Generator の拡張ができるようになりました。

any や all も欲しいのでこちらも実装してみましょう。

今回は Generator を返す必要がないので折角なのでアロー関数で書いてみましょう。

generator.__proto__.__proto__.any = (predicator = (_ => true)) => {
  for (const item of this) {
    if (predicator(item)) {
      return true;
    }
    return false;
  }
}

さて実行してみましょう。

const sample = (function*() {
  yield 1;
  yield 2;
  yield 3;
})();
if (sample.any()) {
  console.log("success"):
}

実行すると以下のエラーが発生します。

VM1280:2 Uncaught TypeError: this is not iterable
    at Generator.generator.__proto__.__proto__.any

this が iterable じゃないと言われています。なるほど?

試しに function で再実装してみましょう。

generator.__proto__.__proto__.any = function(predicator = (_ => true)) {
  for (const item of this) {
    if (predicator(item)) {
      return true;
    }
    return false;
  }
}

そして実行。

if (sample.any()) {
  console.log("success"):
}

見事動きました。何故でしょう?

MDN を見ると以下のように記述されています。

this, arguments, super, new.target を束縛しません。

つまり this は記述されている環境に依存した値のまま変わらないということになります。

アロー関数は関数の引数に使用するなどに止めるのが良さそうですね。

*1:iterable protocol を満たす場合に for of を使うことができる

日本版 HUAWEI Mate 30 Pro 5G を先行販売で買ったので軽くレビュー

ご存知の方も多いと思いますが、HUAWEIアメリカからの制裁によって Google Mobile Services (以下 GMS) を載せることができなくなっています。

日本で出る端末では HUAWEI nova 5T までは載っていますが、これは割と特殊事例で、基本的には今後販売する端末については GMS は載せられません。

そんな中 GMS 非搭載端末として初めて日本で販売される Mate 30 Pro を先行販売で購入してきたので使い勝手やカメラの性能などを軽くレビューしていきたいと思います。

f:id:tyhe:20200329125215j:plain
こちらは Mate 30 Pro で撮ったピザ

続きを読む

楽天モバイル(MNO)で eSIM のアクティベートに失敗した話

前置き

https://network.mobile.rakuten.co.jp/cms/faq/detail/00000890

楽天回線でeSIMに対応している製品はRakuten Miniのみとなります。 その他のeSIM対応製品の動作確認は保証しておりませんのでご了承ください。

と、あるように Rakuten Mini 以外での動作は保証されていませんのでご注意ください。

続きを読む