2014年8月25日

メソッドの型推論で型パラメータの制約は使われない

[サンプルコード]

static class TestTypeInference
{
    static void Main()
    {
        // コンパイル NG
        DoSomething(new Dictionary<string, int[]>());

        // コンパイル OK
        DoSomething<int[], int>(new Dictionary<string, int[]>());
    }

    static void DoSomething<TCollection, TItem>(IDictionary<string, TCollection> map)
        where TCollection : ICollection<TItem>
    {
        Console.WriteLine("DoSomething");
    }
}

DoSomething メソッドは、map 引数の型から TCollection は推論できるが、TItem は推論できない。
TCollection の制約から分かりそうなもんだが、推論してくれない。

あきらめて型パラメータを指定してあげましょう、という話。

検証環境

Windows 7 64bit/Visual Studio 2010 SP1/.NET 4.0

参考URL

2014年8月22日

4Byte 文字を含んだ文字列の文字列挙

普通、文字列を foreach で回すだけで文字列挙は可能だが、4Byte 文字が混じっていると話が違ってくる。
4Byte 文字はその名のとおり 4Byte なので char 型 2 個分に相当する。
※char 型 は 2Byte、.NET の内部文字コードは UTF-16

[文字列挙サンプル]

static void Main()
{
    var text = "鮭𩸽"; // さけ(2Byte) ほっけ(4Byte)
    Console.WriteLine(text.Length);
    foreach( var c in text ) Console.WriteLine(c);
}
[結果]
3
鮭
�
�
※UTF-8 コードページによる表示なので、実行環境は PowerShell ISE 推奨

StringInfo

では、4Byte 文字も 1 文字として扱いたい場合なのだが、StringInfo が用意されている。
列挙に関しては、StringInfo.GetTextElementEnumerator があるが、TextElementEnumerator を返すため、少々使い勝手が悪い。
そこで、次の構造体(とおまけの拡張メソッド)を作成。

public struct TextElementEnumerable : IEnumerable
{
    public string Source;

    public TextElementEnumerator GetEnumerator()
    {
        return StringInfo.GetTextElementEnumerator(Source);
    }
    IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator(); }

    public static TextElementEnumerable Create(string source)
    {
        return new TextElementEnumerable() { Source = source };
    }
}

// TextElementEnumerator が IEnumerator しか実装していないため、
// IEnumerable<string> が欲しい場合はキャストが必要
// ということを考慮した拡張メソッド
public static class TextExtensions
{
    public static IEnumerable<string> GetTextElementEnumerable(this string source)
    {
        if( source == null ) throw new ArgumentNullException("source");
        return TextElementEnumerable.Create(source).Cast<string>();
    }
}

[使用例]

static void Main()
{
    var text = "鮭𩸽"; // さけ(2Byte) ほっけ(4Byte)
    // 4Byte 文字を考慮した文字数
    Console.WriteLine(new StringInfo(text).LengthInTextElements);
    // 4Byte 文字を考慮した文字列挙
    foreach( var c in text.GetTextElementEnumerable() ) Console.WriteLine(c);
}
[結果]
2
鮭
𩸽

参考URL

2014年8月13日

自作シリアライズの注意事項 #1 コレクション

ISerializable で自作シリアライズを実装する際、デシリアライズ処理におけるコレクション(配列やList)の扱いには注意が必要である。
コレクションのデシリアライズ直後、中身はまだデシリアライズされてない。

[ISerializable サンプル]
※本来、これらのプロパティだけなら ISerializable の実装は不要

[Serializable]
public class Node<T> : ISerializable
{
    public Node(){}
    public T Value;
    public readonly List<Node<T>> Children = new List<Node<T>>();

    private Node(SerializationInfo info, StreamingContext context)
    {
        Value = (T)info.GetValue("Value", typeof(T));
        Children = (List<Node<T>>)info.GetValue("Children", typeof(List<Node<T>>));

        Console.WriteLine("コレクションの個数 : {0}", Children.Count);
        foreach( var child in Children ) Console.WriteLine(child == null);
    }

    [SecurityPermissionAttribute(SecurityAction.Demand, SerializationFormatter=true)]
    public virtual void GetObjectData(SerializationInfo info, StreamingContext context)
    {
        info.AddValue("Value", Value);
        info.AddValue("Children", Children);
    }
}

上記オブジェクトをシリアライズ→デシリアライズしてみる。

static class TestSerialization
{
    static void Main()
    {
        var node = new Node<string>() { Value = "parent" };
        node.Children.Add(new Node<string>() { Value = "one" });
        node.Children.Add(new Node<string>() { Value = "two" });

        var mem = new MemoryStream();
        var formatter = new BinaryFormatter();
        formatter.Serialize(mem, node);

        mem.Position = 0;
        var deserialized = (Node<string>)formatter.Deserialize(mem);
        Console.WriteLine("デシリアライズ後");
        Console.WriteLine(deserialized.Children[0].Value);
        Console.WriteLine(deserialized.Children[1].Value);
    }
}

結果は、
コレクションの個数 : 2
True
True
コレクションの個数 : 0
コレクションの個数 : 0
デシリアライズ後
one
two

コレクションのデシリアライズ直後は、中身が null であることが分かる。
その後、コレクションの中身のデシリアライズが実行されている。
つまり、コレクションのデシリアライズは、コレクション本体→中身の順で行われる。

OnDeserializedAttribute

デシリアライズ処理において、コレクションの中身にいじりたい場合はどうするのか?
例えばシリアライズ対象外のプロパティ(Parentとか)を設定したり、独自のAddメソッドなどで追加したい場合など。

そのために、OnDeserializedAttribute が存在する。
これを付けたメソッドは、コレクションの中身がデシリアライズされた後に呼び出される。

[OnDeserialized を追加した ISerializable サンプル]

[Serializable]
public class Node<T> : ISerializable
{
    public Node(){}
    public T Value;
    public readonly List<Node<T>> Children = new List<Node<T>>();

    private Node(SerializationInfo info, StreamingContext context)
    {
        Value = (T)info.GetValue("Value", typeof(T));
        Children = (List<Node<T>>)info.GetValue("Children", typeof(List<Node<T>>));

        Console.WriteLine("コレクションの個数 : {0}", Children.Count);
        foreach( var child in Children ) Console.WriteLine(child == null);
    }

    [OnDeserialized]
    private void OnDeserialized(StreamingContext context)
    {
        Console.WriteLine("OnDeserialized : Value={0}", Value);
        foreach( var child in Children ) Console.WriteLine(child.Value);
    }

    [SecurityPermissionAttribute(SecurityAction.Demand, SerializationFormatter=true)]
    public virtual void GetObjectData(SerializationInfo info, StreamingContext context)
    {
        info.AddValue("Value", Value);
        info.AddValue("Children", Children);
    }
}

上と同じ方法でシリアライズ→デシリアライズした結果は、
コレクションの個数 : 2
True
True
コレクションの個数 : 0
コレクションの個数 : 0
OnDeserialized : Value=parent
one
two
OnDeserialized : Value=one
OnDeserialized : Value=two
デシリアライズ後
one
two

コレクションの中身がデシリアライズされているので、変更が可能。

検証環境

Windows 7 64bit/Visual Studio 2010 SP1/.NET 4.0

参考URL