0 と 1 の世界の見習い探検家

2018年02月

Dictionary<TKey, TValue>からキーだけ、あるいは値だけをそれぞれTKey[]型、TValue[]型の配列として取得する方法をご紹介いたします。

辞書の定義

まずはサンプル用の辞書を定義します。

イメージ 1
図1 辞書の定義

// 辞書「dict」の定義
var dict = new Dictionary<string, string>();
dict.Add("りんご", "Apple");
dict.Add("バナナ", "Banana");
dict.Add("もも", "Peach");

表1 辞書の内容

Key

Value

りんご

Apple

バナナ

Banana

もも

Peach



今回は、キーも値もstring型を利用しておりますが、紹介する方法は、型にとらわれないものです。
適宜書き換えて下さい。

Linqを使う方法

Linqは正義です。
C#をお使いになるのであれば、Linqは積極的に利用していきましょう。

イメージ 2
図2 Linqを使う方法

// Linq
string[] keys = dict.Keys.ToArray();
string[] values = dict.Keys.ToArray();

System.Linq名前空間をusingしていることが前提のコードです。
Linq名前空間によってIEnumerable互換型に対して提供される拡張メソッドを利用しております。

Linqを使わない方法

C#のバージョンなどの都合でLinqという素晴らしいものが使えない状況下で利用できるコードです。

イメージ 3
図3 Linqを使わない方法

// Linqを使わない
string[] keys = new string[dict.Count];
string[] values = new string[dict.Count];
int i = 0;
foreach (var item in dict)
{
  keys[i] = item.Key;
  values[i] = item.Value;

  i++;
}

実行結果

取得したkeyの一覧「keys」の内容を改行して出力してみました。

イメージ 4
図4 実行結果

Linqを使った場合、使わなかった場合、実行結果はもちろん両方同じになります。
でも、Linqを使ったほうが簡単ですよね?

気がついたら2月も終わりです。
いつもの感覚でまだ28日やし~~とか考えてましたけど、そういえば2月って28日まででしたね(

気がつけばもう3月。
実は3月生まれで、3月上旬には誕生日を迎えてしまうわけですが、中旬には学生という区分からも外れてしまうため、「20代無職」という身分になります。
仮になにかやらかして、逮捕されたとすれば、「20代無職の○○容疑者」という報道になるわけですね。
気を付けなければ()


2月終盤ということで、花粉症も日に日にしんどくなり始めています。
昨晩はなかなか寝付けないほどでした。
これが3月、4月で更に酷くなると思うと…。

杉の木全部燃えてくれないかなぁ~~~(暴言)
花粉症の人皆さん同じことを考えてると思いますよ。
花粉症の症状が出るだけで大幅にパフォーマンスが低下しますし、出かける気力も削がれてしまいます。
花粉が日本経済に及ぼしている損失を試算して公表して欲しいレベルですね。


では。

Windows Formsのお話です。
WPFでも同じ現象が発生するみたいですが、今回はWindows Formsのサンプルプログラムを交えてご紹介いたします。

テキストボックスの内容が途中で切れる

テキストエディタ機能のあるアプリケーションを作ろうとしているとき、ファイルの内容が最後までテキストボックスに表示されないという現象が発生しました。

var sr = new StreamReader(path);
this.mainTextBox.Text = sr.ReadToEnd();
sr.Close();

ファイルを StreamReader で読み出し、内容を TextBox コントロールの Text プロパティに設定しています。
これで正常に内容をすべて表示できる場合もあれば、表示できない場合も…。


イメージ 1
図1 notepad.exe で開いた結果

イメージ 2
図2 サンプルプログラムで開いた結果

図1、図2 は、同じファイルをそれぞれ Windows 標準のテキストエディタアプリケーション「メモ帳」(notepad.exe) と先程のコードを使用したサンプルプログラムで開いた結果です。
notepad.exe では正常にファイルの内容をすべて表示することができています。しかしサンプルプログラムでは、途中で内容が切れてしまっています。

途中に終端文字を含むファイル

先程の 図1、図2 は、私が意図的に作成した変な内容のファイルを開いた結果でした。
変な内容のファイルとは、ファイルの途中に終端文字が含まれているファイルです。

終端文字
終端文字とは、バイナリで言うところの0番の文字です。
古くから、C言語などで可変長の文字列バッファの終点位置をデータ受け取り側で知るためなどの目的で利用されておりました。

ちなみにファイルもC#のプログラムから吐き出したのですが、その時のコードはこんな感じ。

イメージ 3
図3 終端文字を途中に含むファイルの出力

System.IO.BinaryWriter クラスを利用しております。

追記
本来、bw.Write(0)では、int型として0が書き込まれてしまいます。
本当は、bw.Write((byte)0) という形で、byte型にキャストするべきなのですが、バイト型で0を1バイトだけ書き込むとnotepad.exeで文字コード判定が壊れてしまうため、意図的にbyte型へのキャストは省略しました。
byte型で書き込みを行いたい場合は、
bw.Write((byte)0)
が正しい形です。
紛らわしい表現となっておりますことお詫び申し上げます。

notepad.exeではどうしているのか

そもそも、終端文字が途中に含まれるファイルというのはかなり特殊です。
大体の場合、バイナリファイルなのではないでしょうか。

なぜ、notepad.exeでは正常に開くことができたのでしょうか。

先程のファイルをまずバイナリエディタで開いてみましょう。

イメージ 4
図4 バイナリエディタ

終端文字が含まれていることがわかります。
次にこのファイルを notepad.exe で開いてみます。
そして上書き保存…。
そのまま再びバイナリエディタで開いてみましょう。

イメージ 5
図5 notepad.exe で上書き保存した結果

終端文字だった箇所が空白文字を示す 20 へ置き換えられていることがわかります。
どうやら notepad.exe では、ファイルをテキストボックスに読み出す前に終端文字を空白文字へ置換しているようですね。

notepad.exe でバイナリファイルを開いて上書き保存すればもちろんそのファイルは壊れてしまいます。
それもこういった開いたときの内部的な置換処理があったからなのでしょうね。

まとめ

ファイルの内容をテキストボックスに読み出す過程で途中で切れてしまう原因の1つとして、「ファイルの途中に終端文字が含まれている」ということがあるようです。
終端文字が含まれているファイルをテキストボックスに読み出すためには、終端文字を空白文字などの別の文字へ置換する必要があります。
しかしながら、もちろんそうすればファイルは壊れてしまいます。

ここまで書いておいて変な話ですけど、まず前提として、そういう変なファイルをテキストボックスで扱うのは極力避けましょう…。

IList<T>に互換性があって,要素の追加と削除をイベントで取れる…以外と知らない方が多いことで有名らしい ObservableCollection.
using System.Collections.ObjectModel 名前空間で提供されている便利なコレクション型です.

WPFをお使いの方であれば,誰でもご存知かと思われますが,ちょっとはまってしまった事例についてご紹介いたします.

ObservableCollection

ObservableCollection<T>は,IList<T>と互換性のあるリスト型コレクションの機能を提供してくれます.
List<T>との主な違いは,要素の追加や削除などのコレクションの内容の変化をイベントで取れるというところ.WPFでMVVMやろうとしたときに,リスト系コントロールのItemsSourceなんかで利用されます.

イメージ 1
図1 CollectionChanged イベント

イメージ 2
図2 CollectionChanged イベントのハンドラ

コレクションの中身が変更されると,CollectionChangedイベントが発火し,NotifyCollectionChangedEventArgs型のイベント引数の中にどう言った変更が為されたのか (例えば,要素の追加や削除など) の情報や,追加されたアイテム,削除されたアイテムの情報が格納されます.

今回取り上げるのは,このコレクション変化時に取得できる要素情報のお話です.

Clearメソッドとイベントデータ

ObservableCollection<T> には,IList<T> に定義されている Clear メソッドが実装されております.
Clear メソッドは,その名のとおり全ての要素をコレクションから削除するというもの.
この時私の中で問題になったのが,この時受け取れるイベント引数の中身です.

イメージ 3
図3 CollectionChanged イベントで利用できるデータ

図3には,Clearメソッド実行時に発生した CollectionChanged の中で利用できる変数の内容を示しております.
「sender」は,CollectionChanged の発火元である ObservableCollection のインスタンス,「e」にはこのイベントで受け取れるイベントデータが格納されております.

表1 ObservableCollection と Action の値
メソッド
Actionの値
Add
Add
Move
Move
Remove, RemoveAt
Remove
SetItem
Replace
Clear
Reset


CollectionChangedイベントで受け取れるイベント引数の中のActionは,Clear操作時,Resetという値になるようです.
ここで問題になるのが,Clear操作前にコレクションに格納されていた要素の取得….

アイテムの削除時にはOldItemsで削除されたアイテムを取得することが可能でした.
Resetでも同じことができるやろと高を括っていると,ClearのときはOldItemsがnullになっているため,NullReferenceExceptionを叩きつけられる羽目になります.

OldItemsで取得できないのならコレクション本体からということを企むも,そもそもCollectionChangedは,コレクションに変化が発生した後の話なので,もうコレクション本体には何も残っていません.


Clear時にそれまで入ってたアイテムを取る方法

現状のところ「新しいコレクション型を定義する」などの方法しかないかと思われます.
もう少し良いやり方が無いか現在模索中です。

というわけで,まず思いついたのが,ObservableCollectionの拡張.
ObservableCollectionを使ってたところをまるまる簡単に差し替えができるようにするためには,型の互換性を持たせておく必要があります.となれば,継承でなんとかできれば,それが一番手っ取り早いというものです.

さっそく継承して,Clearメソッド発生時のイベントデータにそれまでのコレクションの内容を放り込むようにしてみました.

イメージ 4
図4 Clearメソッド発生時の処理だけオーバーライドしたクラス

継承してもpublicな項目でoverrideできるものはありません.
しかし,protectedでClearItemsという形でoverride可能なそれっぽいメソッドが存在していました.

というわけでさっそく処理を書いてこれでいけると思い,実行してみた結果.

イメージ 5
図5 OnClearメソッド内で発生した例外

System.ArgumentException: 'Reset 操作は、項目を変更せずに初期化する必要があります。
パラメーター名:action'


Clearメソッド以外のコレクション操作は,通常のObservableCollectionと同様の動作をしました.
しかし,Clearメソッドを呼んだ途端に先ほど実装したClearItemsメソッドの中で例外が,,

NotifyCollectionChangedEventArgsのコンストラクタで例外を吐かれてしまいました.
どうやら,ActionにClearを設定するときは,Itemsには何も設定できないみたいですね.

拡張クラス側に新たに「Cleared」というイベントを実装して,そこでそれまで入ってた内容を取れるようにしたほうが良さそうですね….

時間があるときにサンプルプログラムをGistか何かで公開して,こちらにリンクを貼り付けたいと思います...
簡単なコードですので,わざわざサンプルを公開するほどでもないでしょうが….

では.


追記 2018/03/02
サンプル実装しました。
【C#小物】ExtendedObservableCollection (2018/3/2(金) 午前 1:00)
https://blogs.yahoo.co.jp/a32kita/15756307.html

テストコードとの兼ね合いでGistではなく通常のリポジトリになってしまいましたが、中身は単純です。

NewtonsoftさんのJson.NET、大変便利ですよね。
MITライセンスということで.NET向けの各種SNSのAPIクライアントライブラリなどを中心に様々な場所で利用されております (2018年02月26日 現在)。

Json.NETでinterfaceのデシリアライズを出来るようにしてみようというお話です。

Json.NET


イメージ 1
図1 NuGetで配布されているJson.NET

.NET Framework向けのオープンソースのJSONパースライブラリとして、NuGet等でMITライセンスで配布されております。
.NETに標準で実装されているXMLやSoapXML向けのXmlSerializer、SoapFormatter同様、オブジェクト インスタンスをJSONへシリアライズする機能、JSONから定義済みのクラスを元にオブジェクト インスタンスを生成する機能、この他JSONを.NET上で取り扱うための様々な機能が実装されております。

え? .NET標準ライブラリにもJSON用のシリアライザがあるですって??
いえ、知らないクラスですね…

本記事で取り扱う内容

Json.NETでは、上記で述べた通りJSONへのシリアライズ、JSONからのデシリアライズ機能がございますが、このうちデシリアライズには制約がございます。
  • 型が具体的に指定されていること
  • あるいは、抽象的に指定された型 (interfaceやabstract class) に対する JsonConverter が定義されていること
デシリアライズを行う際、Json.NET はすでに定義されている型の情報を元にJSONを読み込んでインスタンスを生成します。
インスタンスの生成が直接できない型 (例えばinterfaceなど) が指定されている場合、シリアライズを行うことが出来てもデシリアライズには失敗してしまいます。

イメージ 2
図2 interface 型のデシリアライズの例外

Newtonsoft.Json.JsonSerializationException: 'Could not create an instance of type Sample.TestClasses.ITestClass. Type is an interface or abstract class and cannot be instantiated. Path '', line 1, position 2.'

この例外は、対象が具体的な型であっても、型の内部に抽象的な型で宣言されているフィールド (プロパティ)などが含まれていれば発生します。
上記のような例外が発生したのに、型自体は具体的である場合、その型のフィールド (プロパティ)に interface 型で宣言されたフィールド (プロパティ)が存在している可能性があります。ご確認ください。

interfaceで型指定が行われているものに対してデシリアライズを行う場合、次の方法で問題を解決します。

  1. interface 内に JsonConverter がデシリアライズ先を判断するためのヒントとなるフィールド (プロパティ)を定義
  2. デシリアライズに対応させたいインタフェースに対する JsonConverter を定義
  3. interface の定義に対して、上記で定義した JsonConverter を利用させるための属性の付与

サンプルプログラムについて

本記事で取り上げるプログラムは次のような構成です。

  • Sample.Program クラス
  • Sample.TestClasses.ITestClass インターフェイス
  • Sample.TestClasses.Alpha クラス
  • Sample.TestClasses.Beta クラス

あ、ちなみに次のURLで配布中。
ご自由にお使い下さい。
a32kita/JsonInterfaceDeserializeSample
https://github.com/a32kita/JsonInterfaceDeserializeSample

以上のうち「Alpha」「Beta」は「ITestClass」を実装しており、JsonConverter を定義することで、任意のITestClass実装型からシリアライズされたJSONデータからITestClass型へのデシリアライズを実現することを目的とします。

イメージ 3
図3 サンプルプログラムの言語バージョン

なお、本記事で取り上げるサンプルプログラムは、C# 6.0の記法をがっつり使っております。
C# 6.0が使えない環境下でも対して影響は無いかと思いますが、その辺は適宜ビルドエラーを見ながら書き換えをしていただければなと思います。

ちなみに最初の状態は次の通り。
・・・と思ったのですが、文字数制限の都合上、載せられませんでした。
上記リンクからサンプルコードをご覧ください。

この状態ですと、Mainメソッド内のSerializeObject呼び出し部は通常通り実行可能ですが、DeserializeObject で先に紹介した図1のような例外が発生してしまいます。

interface内に型の判断材料となるフィールド (プロパティ)を設置


イメージ 4
図4 JsonConverterがJSONデータから型を判断するイメージ

JSONデータからデシリアライズ処理でインスタンスを生成しますが、この後用意する JsonConverter を通してデシリアライズ処理が実施されるように変更します。
その際、JsonConverterがJSONデータを見て、シリアライズ前は何型であったのかを判断できるようにならなくてはなりません。

イメージ 5
図5 定義したフィールド (プロパティ)

インターフェイスにこのフィールド (プロパティ)を定義したので、もちろんインターフェイスを実装しているクラス (ここでは「Alpha」、「Beta」) にもフィールド (プロパティ)を定義しなくてはなりません。
また、型の判断情報となる値を代入させる必要があります。
このサンプルでは、クラス名をこのフィールド (プロパティ)に格納しておきます。

イメージ 6
図6 型名の格納

別にフィールド (プロパティ)という形でなくても、とにかくJsonConverterが具体的な型を判断する手立てがあれば構いません。


JsonConverter「TestClassConverter」の定義

JsonConverterを定義していきます。
JsonConverterは、Json.NETから提供されるJsonConverterクラスを継承する形で定義します。

実装のお話はサンプルコードをご覧頂いたほうが早そうですね。

さてさて、まずは継承から。Newtonsoft.Json.JsonConverter<T> を継承します。T は、ここでは ITestClass ということになります。
継承しただけなのに早速怒られますが、まずはサクッと NotImplementedException のメソッドを置いてとりあえず黙らせておきましょう。

イメージ 7
図7 継承エラー

まず2つのプロパティ、CanRead、CanWriteのオーバーライドを実装します。CanReadはtrue、CanWriteはfalseを返すようにすれば十分です。
コンバータではJSONの読み取りのみ対応していることを示すためです。この場合、JSONの書き込みの際は標準の処理が実行されます。

次に先程NotImplementedExceptionをぶん投げるだけにしておいたReadJsonとWriteJson。
このうちWriteJsonは、CanWriteでfalseを示すようにしたので、実行されることがなく、NotImplementedExceptionのままで問題ありません。

ReadJsonメソッドは、ざっくり次のような感じ。
public override ITestClass ReadJson(JsonReader reader, Type objectType, ITestClass existingValue, bool hasExistingValue, JsonSerializer serializer)
{
  // JSONの中身を読めるようにする
  var tokens = Newtonsoft.Json.Linq.JToken.ReadFrom(reader);

  // TargetTypeの値を取得
  var targetType = tokens["TargetType"].ToString();

  // 型判断
  switch (targetType)
  {
    case nameof(Alpha):
      return tokens.ToObject<Alpha>();
    case nameof(Beta):
      return tokens.ToObject<Beta>();
    default:
      throw new NotSupportedException($"型 {targetType} は {nameof(TestClassConverter)} ではサポートしておりません。");
  }
}


先にTargetTypeの値だけ取得して、そこから判断して具体的なシリアライズ先の型を決定しているだけです。簡単ですね。

あとは、このJsonConverterをITestClass型のデシリアライズ時に利用させるための属性を付与します。

イメージ 8
図8 属性の付与

これで問題なくITestClassのデシリアライズが為されるはず…であった、
がしかしこの後とんでもない衝撃の事実が我らを待ち受けるのです…

StackOverflowExceptionの回避

実は、先程定義した JsonConverter だけでは不十分なんです。
実行したとき次のような問題が、

イメージ 9
図9 StackOverflowException

先程定義した「TestClassConverter」のWriteJsonメソッドの中にブレークポイントを設置したら、何回も繰り返し呼ばれていることがわかりました。
と、言いますのも先程のプログラムのうち「tokens.ToObject<Alpha>();」がどうも悪さをしているようで。。
詳しい原因は調査していないのですが、ITestClassに置いたJsonConverterの属性がAlphaクラスにも適用されるとJsonSerializer側からみなされているようで、Alphaのデシリアライズ時もITestClassのデシリアライズ時同様、TestClassConverterが呼ばれちゃってるみたいなんですよね。

この問題を回避する方法はいくつかあるのですが、今回は一番わかり易い方法で行こうと思います。
回避法はズバリ「何もしないConverterを定義し、「Alpha」クラスと「Beta」クラスへ適用させてしまえというもの。
受け継がれし属性を幻想殺し(読み「イマジンブレイカー」)してしまおうという強引技です。

イメージ 10
図10 イマジンブレイカー

イメージ 11
図11 クラスへの適用

コードはサンプルプログラムを見てください。
これでやっと本当に完成です。
お疲れ様でした。

イメージ 12
図12 実行結果

珍しく長編となってしまいました。
もしかすると、もしかしなくても、もっと頭の良い方法を使っている方もいらっしゃるかもしれません。

また型名をデシリアライズ時のヒントにする際の注意点なんですが、Activatorなんかを利用してJSONデータの中に書いてあった型名をインスタンス化するような方法は避けて下さい。
JSONデータがネットを彷徨っている間に悪意のある人間に書き換えられてしまった、なんてことがあるととんでもないセキュリティホールになりかねないので。。。
型のヒントの部分に関しては、飽くまでも簡単に説明するために型名を利用しましたが、そこは皆さんのお作りになっているソフトウェアに応じて書き換えてください。

最後に

本記事で取り上げたサンプルプログラムについて、筆者 (あおと) が著作権を主張することはございません。
これ使える、と思ったらご自由にお使いになって下さい。

なお、Json.NET (Newtonsoft.Json) は、Newtonsoft様がMIT Licenseで頒布しております。
詳しくは、Newtonsoft様のウェブサイトをご覧ください。

では。

このページのトップヘ