更新日: 2010 年 4 月 16 日

執筆者: 株式会社ピーデー 川俣 晶

この記事は、「MSDN プログラミング シリーズ」として発行している技術書籍「プロフェッショナルマスター Visual C# 2010 ~最新テクニックをマスターする 35 のテーマ」(日経 BP 社刊) を基にワンポイント テクニックを紹介しています。


センセーション 「おほん、教師のセンセーションである」
ジョシュア 「生徒のジョシュアです。ところで、困ったことになりました」
センセーション 「どうしたんじゃ?」
ジョシュア 「お客さんがある機能を欲しいと言っていて、そのためのデータはプログラム内にあることは分かっています」
センセーション 「では、その情報を読み出して処理すれば良いのではないかね?」
ジョシュア 「それが. . . そのデータは、サードパーティー製のアセンブリ内の private メンバーで、ソース コードも絶対に非公開だというのです」
センセーション 「なるほど! 実は、そういう場合でも、強引にアクセスすることは不可能ではないのじゃ!」

目次

  1. 参照で渡してしまう方法
  2. ラムダ式でアクセスする方法
  3. リフレクションで読み書きする方法
  4. プロパティへのアクセス
  5. 結末

1. 参照で渡してしまう方法

まず、以下のプログラムはコンパイルできません。フィールド x が private メンバーなので、クラス A からはアクセスできないからです。

C#
using System;

class A
{
    public void CountUp(B b)
    {
        b.x++;
    }
}

class B
{
    private int x;
    public void ShowMe1()
    {
        x = 0;
        new A().CountUp(this);
        Console.WriteLine(x);
    }
}

class Program
{
    static void Main(string[] args)
    {
        new B().ShowMe1();
    }
}
 

このため、このプログラムは、x を宣言する行を以下のように書き換えればコンパイルできます。

C#
internal int x;
 

しかし、private なフィールドは永久に外部から書き換えられないわけではありません。実は、以下のように少しだけ書き換えるだけで、x の宣言を書き換えることなく、x の値を変更できます。(ただし、この例では、クラス B を書き換えていますので注意してください。)

C#
using System;

class A
{
    public void CountUp(ref int y)
    {
        y++;
    }
}

class B
{
    private int x;
    public void ShowMe1()
    {
        x = 0;
        new A().CountUp(ref x);
        Console.WriteLine(x);
    }
}

class Program
{
    static void Main(string[] args)
    {
        new B().ShowMe1();
    }
}
 

実行結果:

1

このポイントは参照渡しです。実は、ref キーワードを付けてしまえば、たとえそのメンバーの公開範囲 (アクセス修飾子) が private であっても、その対象を外部から書き換えることができます。

つまり、参照を渡すということは、公開範囲に関係なく、そのメンバーの扱いを外に委ねるということを意味しています。

ページのトップへ


2. ラムダ式でアクセスする方法

参照を直接渡す方法以外に、ラムダ式を渡すという手段もあります。ラムダ式の詳細ついては今後の連載で記載しますが、ひとまず、以下のサンプル コードでは、a を引数に渡して x = a; と処理する関数 (下記の CountUp メソッドの第 1 引数) と、引数なし (void) で x を返り値として渡す関数 (下記の CountUp メソッドの第 2 引数) のぞれぞれを CountUp の引数として渡していると考えてください。

C#
using System;

class A
{
    public void CountUp(Action<int> setter, Func<int> getter)
    {
        setter( getter()+1 );
    }
}

class B
{
    private int x;
    public void ShowMe1()
    {
        x = 0;
        new A().CountUp((a) => { x = a; }, () => x);
        Console.WriteLine(x);
    }
}

class Program
{
    static void Main(string[] args)
    {
        new B().ShowMe1();
    }
}
 

実行結果:

1

この場合のポイントは、書き込む値の確定は外部で行われているが、読み書きそのものはクラス B の内部で行われる点です。このように、処理自体の仕様はクラス B で決めて、その処理の呼び出しを外に委ねることで、クラス内の private フィールドに、外部で確定した値を格納することも可能です。

ページのトップへ


3. リフレクションで読み書きする方法

前述のラムダ式や参照渡しのような小細工を一切省くこともできます。要するに名前と所在さえ明確に分かっていてれば、以前説明 (第 16 回 参照) したリフレクションを使用してそれを読み書きすることができるのです。

C#
using System;
using System.Reflection;

class A
{
    public void CountUp(B b)
    {
        var field = b.GetType().GetField("x", BindingFlags.GetField | BindingFlags.SetField | BindingFlags.NonPublic | BindingFlags.Instance);
        field.SetValue(b, (int)(field.GetValue(b)) + 1);
    }
}

class B
{
    private int x;
    public void ShowMe1()
    {
        x = 0;
        new A().CountUp(this);
        Console.WriteLine(x);
    }
}

class Program
{
    static void Main(string[] args)
    {
        new B().ShowMe1();
    }
}
 

この場合のメリットは、クラス B には何の配慮も必要がないことです。これまでの参照やラムダ式の例と違い、この場合、クラス B にはいっさいの変更をおこなっておらず、工夫を要するのはクラス A だけです。上述のサンプル コードからわかるように、以下の手順で処理することができます。

  • GetType メソッドで型オブジェクトを得ます
  • GetField メソッドでフィールドを得ます
  • BindingFlags の組み合わせで対象を特定します
  • SetValue メソッドで値を書き込みます
  • GetValue メソッドで値を読み出します (ただし object 型として取得されるので、型を合わせる必要があります)

さて、読者の皆さんは、「private であるはずのもの見える」ことに対して抵抗のある方も居られるかもしれません。しかし、この例からもわかるように、private は、セキュリティなどの理由でその存在を隠蔽しているわけではないという点に注意してください。

そもそも、.NET でコンパイルして構築されたアセンブリには、一般に、中間言語 (MSIL) と呼ばれるプログラミング言語 (C#, Visual Basic など) に中立なコードが埋め込まれており、これが .NET Framework のランタイムによって処理されています。ファイル フォーマットや中間言語に関する仕様はオープンであり、ツールさえあれば、たとえ private であろうと、こうした中間言語で記述されたコードを簡単に入手できます。(.NET Framework SDK には、ildasm.exe というユーティリティも付属しています。) そもそも、バイナリであっても、知識と時間のあるエンジニアなら、その中の処理は解析できてしまうことでしょう。

Note: ただし、パフォーマンス上の理由などから、.NET Framework のネイティブ イメージ ジェネレーター (ngen) というユーティリティを使って、ネイティブの実行イメージを作成してコンピューター上に配置することも可能です。

こうした仕組みからもわかるように、C# で記述する private は、"その存在とアクセスを完全に阻止する" ために使うものではありません。(なお、外部からのソース コードの解読を防ぎたい場合には、別途、「難読化」と呼ばれる手法を使って、内部の変数名などを理解が困難な文字列に変換することができます。) private はこうした目的のためのキーワードではなく、そのクラスの "仕様としてあえて見せないようにする" ことを目的としたキーワードであるという点をおぼえておいてください。このため、private だからと言って、パスワード文字列などの機密性の高い情報をこのメンバーに埋め込むことは絶対避けましょう。(これは .NET に限らず、全般に言えることです。)

さて、上記のリフレクションの使用方法 (サンプル コード) には以下の注意点もあります。

  • 難読化によって名前が変わるとアクセスできなくなる (名前で探しているから)
  • フィールドをプロパティに書き換えるとアクセスできなくなる (あくまでフィールドを探しているから)
  • 「名前が違う」、「型に不整合がある」などのエラーは、コンパイル時に発見できないかもしれない (実行時に例外を投げるかもしれない)

実際、この方法はいろいろと罠も多いので、もし他の方法で対処できるならば避けるべきでしょう。しかし、冒頭でジョシュアが述べたような事態が起こった場合は、こうした方法も非常手段として使えるという点をおぼえておいてください。

ページのトップへ


4. プロパティへのアクセス

上記で、クラス B の x の宣言を以下のように書き換えると、かなり面倒な状況が発生します。

C#
private int x { get; set; }
 

例えば、以下のような問題が起きます。

  • 参照渡しを行うサンプルがコンパイルできません (第 5 回 で述べたように、フィールドは参照渡しできますが、プロパティは参照渡しできません)
  • 上述したリフレクションのサンプルが実行できません (あくまでフィールドを探して読み書きするサンプルだからです)

前者には有効な対処がありません。しかし、後者は以下の通り対処可能です。

C#
using System;
using System.Reflection;

class A
{
    public void CountUp(B b)
    {
        var prop = b.GetType().GetProperty("x", BindingFlags.GetProperty | BindingFlags.SetProperty | BindingFlags.NonPublic | BindingFlags.Instance);
        prop.SetValue(b, (int)(prop.GetValue(b,null)) + 1,null);
    }
}

class B
{
    private int x { get; set; }

    // 以下は同じ . . . (省略)
 

修正された点は以下の通りです。

  • GetField メソッドではなく GetProperty メソッドを使用しています
  • BindingFlags で GetField/SetField ではなく、GetProperty/SetProperty を使用しています
  • GetValue/SetValue メソッドで引数を追加しています

最後の修正は要注意です。これだけは、引数の個数が違ってしまうので、機械的な置き換えでは済まされません。この追加された引数は「インデックス付きプロパティのインデックス値」ですが、「インデックス付きプロパティ」以外では null を渡すことが義務づけられていますので注意しましょう。いずれにせよ、フィールドがプロパティに変わっただけでも、いくつかの手間のかかる修正が必要になってきます。

ちなみに、フィールド、プロパティ以外の private な要素に外部からアクセスする際は、GetMethod メソッドなどが使用できます。

ページのトップへ


5. 結末

ジョシュア 「分かりました。 private を付ければ絶対に外部からアクセスできないと安心はできないわけですね!」
センセーション 「うむ。その気になってアクセスする気になれば不可能ではないのじゃ」
ジョシュア 「でも、これ面倒ですね . . .」
センセーション 「面倒だからこそ、private では、うっかり使ってアクセスしてしまうことを防止できているのじゃ。簡単にアクセスできてしまっては、そもそも private の意味がないからのう。しかし、最後の手段としては有効ということじゃ!」

Code Recipe Code Recipe

ページのトップへ