nullポインターがokな、オブジェクトをC#で実装する

WCFサービスを調べているときに見つけたので、ちょっとメモ的に。下記の SafeInvoke メソッドのところです。

Method call if not null in C# – Stack Overflow
http://stackoverflow.com/questions/872323/method-call-if-not-null-in-c-sharp

objective-c には便利な機能があって、変数が null の場合はメソッドを呼び出さないのです。このために null チェックがいりません。具体的にコードを示すと、

1
2
3
4
5
6
7
NullObject *obj = [NullObject new];
[obj callMethod];
 
// null を代入
obj = NULL;
// 次の関数は呼び出されない
[obj CallMethod];

ってな感じで、2回目の CallMethod は呼び出されません。 if ( obj != NULL ) というチェックがいらなくなってコードがシンプルになります。まあ、厳密性を重んじるならば NULL チェックをする「意図」は残しておいたほうがいいのですが、コードの安全性を考えるとこれで ok な気がします。

■拡張メソッドを使う

実は C# の拡張メソッドを使うと似たようなことができる、というのを先日知りました。元のクラスを NullObject にして、拡張メソッドを含むクラスを NullObjectExtention にしておきます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
namespace SampleNullObjectExtention
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }
 
        private void button1_Click(object sender, EventArgs e)
        {
            var obj = new NullObject();
            Debug.Print("goMessage pre");
            obj.goMessage("masuda");
            Debug.Print("goMessage after");
 
            obj = null;
 
            Debug.Print("goMessageEx pre");
            obj.goMessageEx("tomoaki");
            Debug.Print("goMessageEx pre");
 
        }
    }
 
    public static class NullObjectExtention
    {
        public static void goMessageEx( this NullObject obj, string msg)
        {
            if (obj != null)
            {
                obj.goMessage(msg);
            }
        }
    }
    public class NullObject
    {
        public void goMessage(string msg)
        {
            Debug.Print("in goMessage {0}", msg);
        }
    }
}

すると、button1 をクリックしたときに、obj を null に設定しておいても、goMessageEx メソッド呼び出しは大丈夫なんですね。なるほど。拡張メソッド側で this で参照させて null チェックをするという技です。LINQ の内部でも使っているのかもしれません。

なかなか面倒ですが、メソッドを全てラップしてしまって、ラップした方のメソッドを使うというルールにすれば ok なんですが、いやいや、そうはいきません。インテリセンスで「goMessage」が出てくるならば、それを使ってしまうかもしれない。ならば、obj が null の場合は通常通り落ちてしまう訳です。使えない技ですね~という感じになってしまいます。

■別のアセンブリに隠す(internal protected)

C++のようにfriendで制限ができればよいのですが、.NETにはありません。その代り「internal protected」というちょっと中途半端な(便利な)範囲設定ができます。
拡張メソッドは、protected なメソッドを呼び出せないので、代替案という訳です。

テスト用のプロジェクトとは別に、クラスライブラリを作成します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
namespace CheckNullObjectLib
{
    ///
    /// NullObject の拡張クラス
    ///
    public static class NullObjectExtention
    {
        ///
        /// SetMessage の Wrapper
        ///
        /// <param name="me" />
        /// <param name="msg" />
        public static void SetMessage(this NullObject me, string msg)
        {
            if (me != null)
            {
                me._setMessage(msg);
            }
        }
        ///
        /// GetMessage の Wrapper
        ///
        /// <param name="me" />
        /// <param name="msg" />
        public static string GetMessage(this NullObject me)
        {
            if (me != null)
            {
                return me._getMessage();
            }
            // あえて空文字列を返す
            return "";
        }
    }
    ///
    /// 本体の NullObject クラス
    ///
    public class NullObject
    {
        protected internal string _msg = "";
 
        ///
        /// アクセス制限を protected internal(同じアセンブリのみ)にしておく
        ///
        /// <param name="msg" />
        protected internal void _setMessage(string msg)
        {
            _msg = msg;
        }
        protected internal string _getMessage()
        {
            return _msg;
        }
    }
}

本体の NullObject クラスで公開するメソッド/プロパティは、全て protected internal にしておきます。こうすると、同じアセンブリ内のクラスからのみアクセスが可能になります。これを、ラップする NullObjectExtention クラスでは、public で拡張メソッドを作る訳です。

テストコードはこんな感じになります。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
using CheckNullObjectLib;
 
namespace CheckTestNullObject
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }
 
        private NullObject obj;
 
        ///
        /// set button
        ///
        /// <param name="sender" />
        /// <param name="e" />
        private void button1_Click(object sender, EventArgs e)
        {
            obj = new NullObject();
            obj.SetMessage(textBox1.Text);
        }
 
        ///
        /// get button
        ///
        /// <param name="sender" />
        /// <param name="e" />
        private void button2_Click(object sender, EventArgs e)
        {
            // いきなり set button を押して,
            // obj が null の状態でも大丈夫
            textBox2.Text = obj.GetMessage();
        }
    }
}

内部で保持している NullObject オブジェクト obj は、set button を押したときに new する訳ですが、間違って get button を押したとしても GetMessage メソッドで例外が発生しません。なんと便利なッ!!! objective-c の技がッ!!! って思いますが、まぁ、コーディングの基礎しては「きちんと obj は初期化しておこうね」とか「get button を押した時に null チェックをしようね」というのが C# のコーディングとして正しいやり方ですね。

■どんなところで使うのか?

使い処としては、null チェックを省くってのもそうなのですが、メソッドチェーンのところで途中に null を含められるというメリットがあります。
LINQ の定番として、

1
2
var data = ...
var result = data.From( ... ).Where( ... ).Select( ... ).OrderBy( ... );

という書き方をした場合、From, Where, Select メソッド null を返すと不意に落ちてしまう訳です。ひとつの方法としては、それぞれのメソッドが null を返さないように実装すればよいのです。もうひとつの方法としては、先の拡張メソッドのテクニックを使って null で落ちないという方法が使えます。

まあ、変数を敢えて null で返したいときに使えるかなぁと。

カテゴリー: C# パーマリンク

nullポインターがokな、オブジェクトをC#で実装する への1件のコメント

  1. ピンバック: .NET Clips

コメントは停止中です。