オブジェクト初期化子はやっぱ便利

以前、オブジェクト初期化子について勉強しましたが、その時は何が便利なのかいまいちわかりませんでした。

しかしいくつかのプログラムを書いてる内に、なるほどこれは便利だと思うようになりました。

using System;

class Class {
    public string Name;
}

class Program {
    static void Output(Class cobj) {
        Console.WriteLine(cobj.Name);
    }
    static void Main() {
        var cobj = new Class();
        cobj.Name = "foo";
        
        Output(cobj);
    }
}
$ main
foo

上記処理は一時変数cobjを定義し、Nameに値を設定しているわけですが、オブジェクト初期化子を使えば式として扱えるため、一時変数を省略することができるので処理が簡素になります。

using System;

class Class {
    public string Name;
}

class Program {
    static void Output(Class cobj) {
        Console.WriteLine(cobj.Name);
    }
    static void Main() {
        // オブジェクト初期化子を使ってそのまま渡す
        Output(new Class() { Name = "foo" });
    }
}

便利ですね

演算子のオーバーロード

演算子のオーバーロード - C# によるプログラミング入門 | ++C++; // 未確認飛行 C


C++と同様にC#にも演算子オーバーロードがあります。定義の仕方もまぁほぼ同じですね。

using System;

class Class {
    public int i;
    
    public Class (int i) {
        this.i = i;
    }
    
    // Class同士の足し算
    public static Class operator+ (Class x, Class y) {
        return new Class(x.i+y.i);
    }
    
    // Classとintの足し算
    public static Class operator+ (Class x, int y) {
        return new Class(x.i+y);
    }
    
    // intとClassの足し算
    public static Class operator+ (int x, Class y) {
        return new Class(x+y.i);
    }
}

class Program {
    static void Main() {
        var cobj1 = new Class(1);
        var cobj2 = new Class(10);
        
        var cobj3 = cobj1 + cobj2;
        Console.WriteLine(cobj3.i);
        
        var cobj4 = cobj1 + 50;
        Console.WriteLine(cobj4.i);
        
        var cobj5 = 100 + cobj2;
        Console.WriteLine(cobj5.i);
    }
}
$ main
11
51
110

何がオーバーロード可能かは上記URLに書いてあるので一通り覚えときましょうー。

インデクサー

インデクサー - C# によるプログラミング入門 | ++C++; // 未確認飛行 C


配列の[]をオーバーロードできる機能です。

定義の仕方はプロパティと似ています。

using System;

class Class {
    public int[] data = new int[10];
    
    public int this[int i] {
        set { data[i] = value; }
        get { return data[i]; }
    }
}

class Program {
    static void Main() {
        var cobj = new Class();
        
        cobj[0] = 10;
        
        Console.WriteLine(cobj[0]);
    }
}
$ main
10

いいですね。ただしforeachが使えないみたいなので、こういった処理を書くんであればICollectionやIList等を使って実装した方が良いとのことです。

リフレクションを利用して文字列からクラス操作

実行時型情報 - C# によるプログラミング入門 | ++C++; // 未確認飛行 C


リフレクションとはプログラム実行時にクラス等のメタデータを取り出したりなんかしたりできる機能みたいです。

これだけだとなんのこっちゃわからないのですが、要するに文字列からインスタンスを生成したりとか色々できる機能って感じですかね。

using System;

class Class {
    public int i;
    
    public void output (int j) {
        Console.WriteLine(i+j);
    }
}

class Program {
    static void Main() {
        var cobj = new Class();
        cobj.i = 100;
        cobj.output(50);
    }
}
$ main
150

さて上記のような単にクラスをnewして値を設定してメソッドを呼ぶだけの単純な処理を、リフレクションを使って書き直してみます。

using System;
using System.Reflection;

class Class {
    public int i;
    
    public void output (int j) {
        Console.WriteLine(i+j);
    }
}

class Program {
    static void Main() {
        // Class型の情報
        Type t = Type.GetType("Class");
        
        // var cobj = new Class();の部分
        object o = Activator.CreateInstance(t);
        
        // cobj.i = 100;の部分
        t.GetField("i").SetValue(o,100);
        
        // Class.outputメソッド情報
        MethodInfo m = t.GetMethod("output");
        
        // cobj.output(50);の部分
        m.Invoke(o,new object[]{ 50 });
    }
}

このようになります。クラス名やフィールド名等、全て文字列で指定することが可能となってます。面白い機能ですね。

ちなみに上記処理をコンパイルすると以下のような警告がでます。

$ csc main.cs
Microsoft (R) Visual C# 2010 Compiler version 4.0.30319.1
Copyright (C) Microsoft Corporation. All rights reserved.

main.cs(5,13): warning CS0649: フィールド 'Class.i' は割り当てられません。常に既定値 0 を使用します。

恐らくこれは、、リフレクションを利用してiに値を設定しているのでコンパイルではiに値が設定されてるかどうかが検出できないために出る警告のようですね。

またメソッド呼び出しの引数の型がobjectになったりと、コンパイル時チェックが働かないのでリフレクションの機能を使う場合は注意が必要ですね。

あと速度的にもかなり遅いらしく、一度だけ通る処理等であれば体感出来る程ではないものの、ループ等で繰り返し処理するようなケースではあまり使わない方が良いみたいです。

使いどころが難しいですね。

ちなみに

    // Class型の情報
    Type t = Type.GetType("Class");

の部分ですが、typeofを使って以下のようにも書けます。

    // Class型の情報
    Type t = typeof(Class);

ただし、typeofは型名を指定しないといけないので文字列から型情報を取るGetTypeの方と使い分けが必要ですね。

Conditional属性

属性 - C# によるプログラミング入門 | ++C++; // 未確認飛行 C


属性というものが出てきました。

クラスやメソッド等に追加情報を与えることができる機能とのことです。

「百聞は一実装にしかず」という諺もある通り、手を動かしてみましょう。

色々な属性があるみたいですが、まずはシンプルなConditional属性について学んでみましょう。

Conditional属性とはdefineで定義されたシンボルが存在しているときだけ有効な処理を定義することができます。

using System;
using System.Diagnostics;

public static class Class {
    // DEBUGシンボルが定義されている場合のみ有効なメソッド
    [Conditional("DEBUG")]
    public static void debugOutput (int i) {
        Console.WriteLine(i);
    }
}

class Program {
    static void Main() {
        Class.debugOutput(100);
    }
}

さてこれを、以下のようにコマンドラインオプションに/defineを追加してコンパイルします。

$ csc /define:DEBUG main.cs

そして実行すると

$ main
100

と表示されます。では次に/define無しコンパイルしてみます。

$ csc main.cs
Microsoft (R) Visual C# 2010 Compiler version 4.0.30319.1
Copyright (C) Microsoft Corporation. All rights reserved.


$ main

コンパイルエラーも出ないし、実行しても何も出力されません。

このようにConditional属性を使うと、関数の存在そのものが消えてなくなります。

これは非常に強力で便利な機能で、もし同じような処理をConditional属性を利用せずに書こうと思ったら、

using System;

public static class Class {
#if DEBUG
    public static void debugOutput (int i) {
        Console.WriteLine(i);
    }
#endif
}

class Program {
    static void Main() {
#if DEBUG
        Class.debugOutput(100);
#endif
    }
}

このように定義時、呼び出し時全てにおいてわざわざ#if DEBUG〜#endifを書かなければなりません。

というわけで今後この機能を使ってデバッグ用の処理を書いていきたいと思います。

属性については他にも標準で定義されているものが色々とあるみたいなのでまた追々学んでいきたいとおもいます。

デバッグ時のみ出力したり関数実行したりする

前回の記事でConditional属性について学んだので早速デバッグ用の関数を作ってみました。

-- lib/debug.cs --

using System;
using System.Diagnostics;

namespace MyLib
{
    public static class debug
    {
        // デバッグ改行出力関数
        [Conditional("DEBUG")]
        public static void wl (object s) {
            Console.WriteLine(s);
        }
        [Conditional("DEBUG")]
        public static void wl (string s, params object[] obj) {
            var sout = string.Format(s,obj);
            Console.WriteLine(sout);
        }
        
        // デバッグ出力関数
        [Conditional("DEBUG")]
        public static void w (object s) {
            Console.Write(s);
        }
        [Conditional("DEBUG")]
        public static void w (string s, params object[] obj) {
            var sout = string.Format(s,obj);
            Console.Write(sout);
        }
        
        // デバッグ時のみ有効な関数を実行
        [Conditional("DEBUG")]
        public static void f (Action func) {
            func();
        }
    }
}
-- main.cs --

using System;
using MyLib;

class Program {
    static void Main() {
        Console.WriteLine("foo1");
        
        // DEBUG時のみ出力
        debug.wl("foo2");
        
        Console.WriteLine("foo3");
        
        // DEBUG時のみ実行される処理
        debug.f(() => {
            Console.WriteLine("foo4-1");
            Console.WriteLine("foo4-2");
            Console.WriteLine("foo4-3");
        });
        
        Console.WriteLine("foo5");
    }
}

DEBUG有り

$ csc /define:DEBUG main.cs lib\*.cs
Microsoft (R) Visual C# 2010 Compiler version 4.0.30319.1
Copyright (C) Microsoft Corporation. All rights reserved.


$ main
foo1
foo2
foo3
foo4-1
foo4-2
foo4-3
foo5

DEBUG無し

$ csc main.cs lib\*.cs
Microsoft (R) Visual C# 2010 Compiler version 4.0.30319.1
Copyright (C) Microsoft Corporation. All rights reserved.


$ main
foo1
foo3
foo5

うまくいってますね。今後使って行きたいと思います。