C#の0がenum値に暗黙キャストされてinvokeStaticで例外出て超ハマった
僕はついさっき知ったばかりなんですけど、C#の数値0ってあらゆるenum値に暗黙的キャストされるんですってね。要するに、0だけはキャスト不要で列挙型の値として変数に代入できちゃったりするということ。コードで示すと以下のようなかんじ。
using System; public class Program { enum Hoge { A, B } enum Piyo { C, D } public static void Main() { Hoge h1 = 0, h2 = 0.0f; Piyo p1 = 0x0, p2 = 0.0m; Console.WriteLine($"{h1}, {h2}"); Console.WriteLine($"{p1}, {p2}"); } }
このコードは何の警告もなくビルドが通り、以下の実行結果が得られる。
A, A C, C
浮動小数点数やDecimalのゼロも同様の扱いっていうのが、なかなかのキモいポイント。C#のenumってC/C++のそれと違ってそれなりに厳密なので、自分的には結構衝撃的な仕様だった。まぁ、分かってしまえばどうってことはない。
で、ここからが本題。
そんな0のenumへの暗黙的型変換のおかげで、意図せぬメソッドのオーバーロード解決が行われ、小一時間ハマった。
以下のような処理メソッドAdd
と、それに対する単体テストAddTest
を考える。対象メソッドはプライベートな静的メソッドなので、テスト呼び出しにはPrivateType.InvokeStatic
メソッドを使っているが、至ってシンプルなコードである。
何の疑いもなく動くと思いきや、ケース3のテストでのみMissingMethodException
例外が発生し、Add
メソッドの呼び出しに失敗するのだ!
// 加算 private static decimal? Add(decimal? v1, decimal? v2) { return v1 + v2; } // Addメソッドの単体テスト void AddTest() { var privateType = new PrivateType(typeof(AddClass)); // ケース1 var value = (decimal?)privateType.InvokeStatic("Add", 1.0m, 1.0m); // OK Assert.AreEqual(2, value); // ケース2 value = (decimal?)privateType.InvokeStatic("Add", 1.0m, 0.0m); // OK Assert.AreEqual(1, value); // ケース3 value = (decimal?)privateType.InvokeStatic("Add", 0.0m, 1.0m); // MissingMethodException例外が発生! Assert.AreEqual(1, value); }
MissingMethodException
は読んで字のごとく、メソッドが見つからなかった場合に投げられる例外だ。
ケース1~2は通っているのに見つからないとは一体…!?という感じなのだが、これまでの説明からお気づきであろう、ケース1~2と3では呼び出されるInvokeStaticのシグネチャが違うのだ。同メソッドは引数違いで10個ほどのオーバーロードが定義されており、それぞれ以下のシグネチャのものが呼び出される。
- ケース1~2
- InvokeStatic(String, params Object[])
- ケース3
- InvokeStatic(string, System.Reflection.BindingFlags, params Object[])
ケース3では、第二引数の0.0mがBindingFlagsに暗黙キャストされた結果、引数を1つ持つAdd
メソッドを呼び出そうとして例外を吐くというわけ。なんじゃそりゃー!分かるわけネェーッ!!意図通り動かすにはprivateType.InvokeStatic(“Add”, (decimal?)0.0m, 1.0m)
のように、明示的にキャストし正しいオーバーロード解決を導いてあげればよい。
この暗黙型変換を禁止ないし警告出してくれるコンパイルオプションとかないのかしら…ハマった時に死ぬほどわかりづらいんですけど……
C#でローカルのDTDファイルを使うXMLリゾルバを作る
以前、C#でXmlDocumentを作る時にリゾルバでタイムアウトすると書いたが、ようやくローカルのDTDファイルを使ったXMLリゾルバが作れたので、コードをまるっと公開。尚、.NET Framework 4では殆ど同じことを行うXmlPreloadedResolver
クラスが追加されているので、使えるならそっちを使うのが良い。悲しいかな、うちは.NET 3.5なのさ……
using System; using System.Collections.Generic; using System.Xml; using System.IO; namespace ProductionKusoGA { class LocalXmlResolver : XmlResolver { public LocalXmlResolver() { } public override System.Net.ICredentials Credentials { set { } } public override Uri ResolveUri(Uri baseUri, string relativeUri) { Uri uri = DocTypeManager.Instance.GetDTDURI(relativeUri); return uri != null ? uri : base.ResolveUri(baseUri, relativeUri); } public override object GetEntity(Uri absoluteUri, string role, Type ofObjectToReturn) { object entity = DocTypeManager.Instance.GetDTDStream(absoluteUri.AbsoluteUri); if (entity == null) { XmlUrlResolver resolver = new XmlUrlResolver(); entity = resolver.GetEntity(absoluteUri, role, ofObjectToReturn); } return entity; } class DocTypeManager { public static readonly DocTypeManager Instance = new DocTypeManager(); public Uri GetDTDURI(string inFPI) { Uri dtdURI = null; URIForFPI.TryGetValue(inFPI, out dtdURI); return dtdURI; } public FileStream GetDTDStream(string inURI) { FileStream stream = null; if (DTDStreamForURI.TryGetValue(inURI, out stream) == false) { string dtdFile = null; if (DTDFileForURI.TryGetValue(inURI, out dtdFile)) { string RESOURCE_DIR = "..."; stream = new FileStream(Path.Combine(RESOURCE_DIR, dtdFile), FileMode.Open); DTDStreamForURI.Add(inURI, stream); } } return stream; } DocTypeManager() { URIForFPI = new Dictionary<string,Uri>(); DTDFileForURI = new Dictionary<string,string>(); DTDStreamForURI = new Dictionary<string, FileStream>(); AddDTD("-//W3C//DTD XHTML 1.0 Strict//EN", @"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd", @"path/to/xhtml1-strict.dtd"); AddDTD("-//W3C//DTD XHTML 1.0 Trasitional//EN", @"http://www.w3.org/TR/xhtml1/DTD/xhtml1-trasitional.dtd", @"path/to/xhtml1-trasitional.dtd"); AddDTD("-//W3C//DTD XHTML 1.0 Frameset//EN", @"http://www.w3.org/TR/xhtml1/DTD/xhtml1-frameset.dtd", @"path/to/xhtml1-frameset.dtd"); AddDTD("xhtml-lat1.ent", @"http://www.w3.org/TR/xhtml1/DTD/xhtml-lat1.ent", @"path/to/xhtml-lat1.ent"); AddDTD("xhtml-symbol.ent", @"http://www.w3.org/TR/xhtml1/DTD/xhtml-symbol.ent", @"path/to/xhtml-symbol.ent"); AddDTD("xhtml-special.ent", @"http://www.w3.org/TR/xhtml1/DTD/xhtml-special.ent", @"path/to/xhtml-special.ent"); } ~DocTypeManager() { foreach (var pair in DTDStreamForURI) { if (pair.Value != null) { pair.Value.Dispose(); } } } void AddDTD(string inFPI, string inURI, string inFilepath) { URIForFPI.Add(inFPI, new Uri(inURI)); DTDFileForURI.Add(inURI, inFilepath); } Dictionary<string, Uri> URIForFPI { get; set; } Dictionary<string, string> DTDFileForURI { get; set; } Dictionary<string, FileStream> DTDStreamForURI{ get; set; } } } }
DTDファイルのパスを適宜変更し、xmlDocument.Resolver = new LocalXmlResolver();
ってな感じで設定してやればおk。DTDを増やしたいときはAddDTD
を増やす。DTDが内部で参照しているファイル(上記コードで言えばxhtml-lat1.ent
とか)も漏れなく追加する必要がある。
一応、コードのライセンスはパブリックドメインってことで。煮るなり焼くなりお好きにどうぞ。
C#のCreateDocumentTypeがタイムアウトする時の簡易対策
C#のXmlDocument
でHTMLを生成しようと、W3CのDTDを指定してXmlDocument.CreateDocumentType()
するとタイムアウトやHTTPステータスコード500で例外を吐くことがある。こちとらvalidなHTMLを生成しようと真面目に指定してんのに、この仕打である(´・ω・`)。みんなW3Cを見に行って慢性的な高負荷状態になってるのが原因らしいが、まぁ当然そうなりますわな…。
コードにすると↓な感じ。
XmlDocument doc = new XmlDocument(); XmlDocumentType docType = doc.CreateDocumentType( "HTML", "-//W3C//DTD HTML 4.01 Frameset//EN", "https://www.w3.org/TR/html401/frameset.dtd", null); /* ここでエラー */
XmlResolver
でローカルキャッシュしたDTDをマッピングするのが正攻法らしいのだが、ぶっちゃけ面倒。というか、調べても良くわからんかったのでパスw(分かったら追記する…多分)
とりあえずエラーを回避するだけなら、CreateDocumentType
実行前にXmlDocument.Resolver
をnull
にしてやればOKっぽい。コードにするまでもないが一応書いておく。
XmlDocument doc = new XmlDocument(); doc.Resolver = null; // 追加 XmlDocumentType docType = doc.CreateDocumentType( "HTML", "-//W3C//DTD HTML 4.01 Frameset//EN", "https://www.w3.org/TR/html401/frameset.dtd", null); /* エラーにならない */
・・・と、ここまで書いて思ったが、これで回避できるって事は自前実装したXMLリゾルバでDTD返してやればいいだけなんじゃね?
2016-04-13追記
といわけでローカルのDTDを使うリゾルバを作った。
C#のstring.Trim()は全角スペースまで削って下さりやがる
C#のstring
クラスにあるTrim()
メソッドは、C#の三大便利関数の1つと言って良いくらい便利な関数だ。
言わずもがな、文字列の先頭と末尾に付随する空白を削除してくれる関数であるが、空白って一体なんなのよというと「Unicodeが定める空白文字」である。従って、Trim()
は文字列の前後からUnicodeが定める空白文字を削除する関数という事になる。(更に言うと.NET Frameworkのバージョンによって細部の挙動が違う。詳細はMSDNを参照の事。)
んじゃんじゃUnicodeの空白文字ってなんぞ?というと、C#的にはChar.IsWhiteSpace(letter) == true
となる文字である。詳細はUnicodeの規格書なりWikipediaなりを見て頂くとして、true判定になる文字には半角スペース(U+0020)やタブ文字(U+0009)は然ることながら、全角スペース(U+3000)も含まれるのだ。つまりTrim()
を使うと全角スペースも奇麗さっぱりなくなっちゃう。なんというか、C/C++の非WIDE文字な文字列操作に慣れている身からすると、直感とは異なる挙動なわけ。
今回はこれにハマった。削られちゃマズい全角スペースが見事になくなってて、Trim()
の挙動を初めて知ったという(´・ω・`)
回避策は引数ありバージョンのTrim()
で、全角スペースを除いた空白文字配列を渡すしかない模様。↓こんな感じで拡張メソッド化しておくと便利に使えるよ(`・ω・´)
public static class MyStringAdditions { static char[] WhiteSpaceDelimiters = new char[] { '\u0009', // CHARACTER TABULATION '\u000A', // LINE FEED '\u000B', // LINE TABULATION '\u000C', // FORM FEED '\u000D', // CARRIAGE RETURN '\u0020', // SPACE '\u00A0', // NO-BREAK SPACE '\u2000', // EN QUAD '\u2001', // EM QUAD '\u2002', // EN SPACE '\u2003', // EM SPACE '\u2004', // THREE-PER-EM SPACE '\u2005', // FOUR-PER-EM SPACE '\u2006', // SIX-PER-EM SPACE '\u2007', // FIGURE SPACE '\u2008', // PUNCTUATION SPACE '\u2009', // THIN SPACE '\u200A', // HAIR SPACE '\u200B', // ZERO WIDTH SPACE // '\u3000', // IDEOGRAPHIC SPACE -- これが所謂全角スペース '\uFEFF' // ZERO WIDTH NO-BREAK SPACE }; public static string TrimWithoutZenkakuSpace(this string str) { string s = str.Trim(WhiteSpaceDelimiters); return s; } }
この件とは直接関係ないけど、TrimStart
とTrimEnd
なんてメソッドもあったんだね。取り除きたい文字の配列を渡すと、対象文字列の先頭もしくは末尾から除去してくれる。Trim
の分割バージョンみたいなやつ、というよりもTrim
がTrimStart
とTrimEnd
の合体技と言った方がいいか。覚えといて損は無さそう。