start

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.Resolvernullにしてやれば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返してやればいいだけなんじゃね?

C#のジェネリックで特殊化っぽいことをする

新年明けましておめでとうございます(遅。開設14年目となるクソゲ~製作所を本年もよろしくお願い致します。

Cのテンプレートには特殊化という、特定の型パラメータの時にテンプレートの実体を別に定義する機能がある。だが、C#版テンプレートとも言えるジェネリックでは、なんということでしょう、特殊化が使えないではありませんか!それをどうにかしてジェネリック特殊化っぽい事をしてみたっていうお話。 こんなコードがあったとする。 <code csharp> public class Node { // 子ノード public List<Node> Children; // 特定の子ノードを取得 public List<T> FindChildren<T>() : where Node { List<T> list = new List<T>(); Type type = typeof(T); foreach (Node e in Children) { if (type == e.GetType()) { list.Add(e); } list.AddRange(e.FindChildren<T>()); } return list; } } public class DocumentNode : Node {} public class PageNode : Node {} public class TitleNode : Node { public string Title; public TitleNode(string title) { Title = title; } } public class SectionNode : Node { public TitleNode Title; } static void Main() { DocumentNode doc = new DocumentNode(); PageNode page = new PageNode(); doc.Children.Add(page); SectionNode section1 = new SectionNode(); section1.Title = new TitleNode("はじめに"); page.Children.Add(section1); SectionNode section2 = new SectionNode(); section2.Title = new TitleNode("つぎに") page.Children.Add(section2); // 全セクションを取得 List<SectionNode> allSections = doc.FindChildren<SectionNode>(); //正しく取得できる // 全タイトルを取得 List<TitleNode> allTitles = doc.FindChildren<TitleNode>(); //★正しく取得できない!! ... } </code> <code> ◆データ構造 [DocumentNode] -Children -[PageNode] -Children -[SectionNode] -Title -Children -[SectionNode] -Title -Children </code> 文章を模したデータ構造を作り、''FindChildren''メソッドで型をパラメータに子ノードを取得しているが、 ''doc.FindChildren<SectionNode>()''は正しい挙動をするものの、''doc.FindChildren<TitleNode>()''の方は空のリストが帰ってくる。''FindChildren()''は''Node.Children''しか見てないので、独立したメンバ''SectionNode.Title''が入るはずはなく当然の挙動である。 こんな時、C++なら特殊化で''FindChildren<TitleNode>''専用の処理が書け同一のインタフェースを提供できるのだが、前述の通りジェネリックは特殊化が使えないの。C#的には''FindChildTitleNodes()''的な別メソッドで提供するのが正しいのかもしれないが、やっぱり統一感に欠けて美しくない(そもそもこんな糞いデータ構造にすんなって話だが例ってことで許してね。) で、知恵を振り絞って''FindChildren''を以下のようにした。 <code csharp> public List<T> FindChildren<T>() : where Node { List<T> list = new List<T>(); Type type = typeof(T); if (type == typeof(TitleNode)) // (1) { List<T> titles = new List<T>(); // (2)-a var sections = FindChildrenInternal<SectionNode>(); section.ForEach(e => { titles.Add(e.Title as T); }); // (3) return titles; // (2)-b } return FindChildrenInternal<T>(); } List<T> FindChildrenInternal<T>() : where Node { List<T> list = new List<T>(); Type type = typeof(T); foreach (Node e in Children) { if (type == e.GetType()) { list.Add(e); } list.AddRange(e.FindChildren<T>()); } return list; } </code> 元の''FindChildren()''を''FindChildrenInternal()''とし、''FindChildren()''には型パラメータ''T''に応じた処理を書くようにした。C++でコンパイラが自動で行ってくれる特殊化による処理の振り分けを、実行時に手動で行うような感じかしら。原理上、実行時コストは増えるが、この程度なら大した影響はないだろう。JIT様もあることだし。 型が増えると''if''と特殊化処理の嵐になるが、型ごとに処理デリゲートを作って''Type''とデリゲート辞書でも作ればすっきりするので大した問題ではない。そもそも、そんな状況は最早「特殊化」の本質から外れてるので設計から見直すべきだろう。 このコードのミソは(1)~(3)の部分。 (1)で処理すべき''T''の型が明確になったものの、(2)-aで''List<T>''としているのはコンパイルを通すため。''List<TitleNode>''でも良さそうに見えるが、''T == TitleNode''になるとは限らないので(2)-bで型不一致エラーになる。''List<TitleNode>''とした場合に''T = SectionNode''で呼び出した時にどうなるかを考えれば、すぐにおかしさに気づいて頂けるかなと。 (3)も一見''titles.Add(e.Title)''で良さそうだが、これまた''TitleNode''が必ずしも''T''に変換可能とは限らないのでCS1503エラーとなる。よって、ちょっとアホくさいが''TitleNode''を自身の型''T''にキャストしてやらなければならない。 メソッドの引数に型パラメータを持てる場面では、[[https://arbel.net/2007/11/22/c-partial-specialization-with-extension-methods/ にも軽量な型情報システムがあればいいのになぁ…。

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;
    }
}

この件とは直接関係ないけど、TrimStartTrimEndなんてメソッドもあったんだね。取り除きたい文字の配列を渡すと、対象文字列の先頭もしくは末尾から除去してくれる。Trimの分割バージョンみたいなやつ、というよりもTrimTrimStartTrimEndの合体技と言った方がいいか。覚えといて損は無さそう。

  • start.txt
  • 最終更新: 2022-07-27 15:26
  • by Decomo