RSS RSS feed | Atom Atom feed

魂のプログラミング

魂のプログラミング

魂のプログラミングとはシンプルな信念である。

プログラムコードは一行一行すべてに魂がこもっていなければならない

という信念である。

以前プログラム製造の現場で若手プログラマを指導している際に, つい「この行には魂がこもっていない」という言い方を何度かしていたところ, 周囲の数人の SE にそのような物言いが伝染してしまった。後日, その中の一人がソースコードレビューに参加したときに「このソースコードには魂がこもっていない」と指摘し, 指摘されたプログラマが反応に窮したというエピソードを聞いた。「魂のプログラミング」という言葉は, このエピソードを聞いたときにふと私の頭の中に浮かんだ言葉である。

「プログラムコードの一行に魂がこもる」とは, 別に宗教めいた意味ではない。一言で言うならば,

プログラムコードのあらゆる行に目的がある

とか

どの一行についてもその目的が必ず説明できる

というような意味である。空行にさえ説明可能な目的があるべきだと私は考えている。

本稿では魂のプログラミングを実践する上での教訓を, 例を挙げながら解説していくことにする。まだまとまっておらず, 多分に断片的であるが, 魂のプログラミングの一端は感じられるのではないだろうか。

やりたいことを簡潔に表現する

やりたいことを正確に理解する

唐突ではあるが, 「endian (エンディアン)」という言葉を聴いたことがあるだろうか。ガリバー旅行記に由来するこの言葉は, 多桁数値を表現する際, 上位桁から下位桁に向かって記述する (big endian) か, 下位桁から上位桁に向かって記述する (little endian) かの約束事を意味している。ちなみに算用数字による 123 などの表記は big endian で, 百二十三を敢えて little endian で書けば 321 となる。太古の昔から多くのコンピュータはどちらかの宗派に属している。(少数派として変態 endian の VAX などや, endianess 切替機能付きコンピュータもある。)

昔, このような質問を受けたことがある。

プログラムを実行しているコンピュータが big endian であるか調べ, もしそうでなかったら 32 ビット値のバイト順を逆転させたい。C でどう書くのがスマートですか

この質問にもし条件反射的に回答するとすれば, たとえば以下のようになる。

/* big endian であるか調べ, そうでなかったらバイト順を逆転するコード。*/
    ...
    static short one = 1;
    unsigned v = ...;
    ...
    if (*(char*)&one != 0) // 注意: 少し危険な判定!
      {
        v = v >> 16 & 0x0000FFFF | v << 16 & 0xFFFF0000;
        v = v >>  8 & 0x00FF00FF | v <<  8 & 0xFF00FF00;
      }

しかし, そう回答する前にこの質問者が本当は何がしたかったのか考えてみよう。3 分ほどじっくり考えると, 実はこの質問者は客先のコンピュータの endianess を研究したいわけでも, バイト順を逆転させるトリックを知りたいわけでもないことに気付く。質問者が本当にやりたかったことは,

4 バイト big endian で格納された 32 ビット値を取出す

という処理である。

/** 4 バイト big endian で格納された 32 ビット値を取出すコード。*/
    unsigned v;
    unsigned char bytes[4];
    ...
    v = bytes[0] << 24 | bytes[1] << 16 | bytes[2] << 8 | bytes[3];

処理は 1 行で完結する。こちらの方がスマートで分かりやすいコードであるが, これは別にプログラミングスキルの問題ではなく, それ以前の問題であったことが分かる。すなわち, 自分が本当は何をしたいのか理解していなかったのである。

最適な手段で表現する

短いコードは常に長いコードより優れている。より良いコードを書きたいなら, 第一に短く簡潔なコードを目指すべきである。

プログラミング言語にはそれぞれ特有の考え方や機能がある。そのため, 最適なコードは利用するプログラミング言語によって変わってくる。この観点からもプログラミング言語に関する正確な知識は重要である。

例として, まず JAVA で素朴な線形リストクラスを実装してみよう。

/** JAVA 版線形リストの実装クラス。*/
public class SinglyLinkedList<T>
  {
    /** 各ノードを表すクラス。*/
    private static class Node<U>
      {
        private Node(U data) { this(null, data); }

        private Node(Node<U> next, U data)
          {
            this.next = next;
            this.data = data;
          }

        private Node<U> next;
        private U data;
      }

    ...

    /**
     * このリストの末尾に指定された要素を追加する。
     *
     * @param o 追加する要素
     */
    public void pushBack(T o)
      {
        Node<T> node = head;
        if (node == null)
          {
            // ノードが一つもなかった場合, 追加する要素のノードが先頭となる。
            head = new Node<T>(o);
          }
        else
          { // 最後のノードを探す。
            while (node.next != null)
                node = node.next;
            // 最後のノードの次に追加。
            node.next = new Node<T>(o);
          }
      }

    ...

    private Node<T> head;
  }

同様な実装を C++ のテンプレートで行う。C/C++ のポインタを効果的に利用すると pushBack メソッド (C++ では push_back メンバ関数) の条件分岐が不要となる。

/** C++ 版線形リストクラステンプレート。*/
template<class T> class singly_linked_list
  {
    /** 各ノードを表す構造体。*/
    struct node
      {
        explicit node(const T& data = T(), node* next = 0):
            m_next(next), m_data(data) { }

        node* m_next;
        T m_data;
      };

  public:

    ...

    /**
     * このリストの末尾に指定された値の要素を追加する。
     *
     * @param o 追加する要素の値
     */
    void push_back(const T& o)
      {
        node** cptr = &m_head;
        for (node* curr = *cptr; curr != 0; curr = *cptr)
            cptr = &curr->m_next;
        *cptr = new node(o);
      }

    ...

  private:
    node* m_head;
  }

なぜこれでよいのかは, じっくり考えてみて欲しい。

アイディアや疑問は実験してみる

次のコード片はコンパイルできるだろうか。もしできたとして実行すると何が起こるのだろうか。

// ふと疑問に思った JAVA のコード片。
    String[] s = { "Hello", "World" };
    Object[] o = s;
    o[0] = new Integer(0);
    System.out.println(s[0].getClass().getName());

JAVA である程度経験を積んだプログラマなら, 答を知っていると思うが, 分からなくても実際やってみることは 3 分もあればできる。

前節で紹介した例の通り, 少なくともポインタがある分 C++ は JAVA よりも表現力で勝っているといえる。しかし, JAVA でも C++ のポインタを真似すれば同様の表現が可能なのではないか, というアイディアを持ったとしよう。

早速頑張ってみよう。

よく JAVA にはポインタがないと言われたり, JAVA にはポインタしかないと言われたりする。JAVA のオブジェクト参照と C/C++ のポインタ (の値) の考え方は似ているが, 決定的に違うのは C/C++ はポインタ型を含めてあらゆる型のデータオブジェクトへのポインタが & 演算子で簡単に作成できるという点である。そこで & 演算子を適用する可能性のあるポインタ (オブジェクト参照) を予め Pointer オブジェクトとしてラップしておけばうまく真似できるのではないかと予想が立つ。詳細な説明は省略するが, その考え方で前節の例を書き直すと, 次のコードとなった。

/** C++ 版の真似をした JAVA 版線形リスト。*/
public class SinglyLinkedListCPlusPlus<T>
  {
    /** X へのポインタを真似るクラス。*/
    private static class Pointer<X>
      {
        private Pointer(X pointee) { this.pointee = pointee; }

        private X pointee;
      }

    /** 各ノードを表すクラス。*/
    private static class Node<U>
      {
        private Node(U data) { this(null, data); }

        private Node(Node<U> next, U data)
          {
            this.next = new Pointer<Node<U>>(next);
            this.data = data;
          }

        private Pointer<Node<U>> next;
        private U data;
      }

    ...

    /**
     * このリストの末尾に指定された要素を追加する。
     *
     * @param o 追加する要素
     */
    public void pushBack(T o)
      {
        Pointer<Node<T>> cptr = head;
        for (Node<T> curr = cptr.pointee; curr != null; curr = cptr.pointee)
            cptr = curr.next;
        cptr.pointee = new Node<T>(o);
      }

    ...

    private Pointer<Node<T>> head;
  }

ひとまず実験は成功である。しかし実験は実験であって, こんな冗談コードを実際の現場では利用するべきではない。この Pointe クラスは if 文を一つ減らすための JAVA での最適な手段とは決して言えない。

境界に注意する

次の中途半端に魂のこもったコードを見てもらいたい。バグが 3 つほど巧妙に仕込んであるのだが, 気付くだろうか。

/* 関数名と引数で関数値を計算する C のコード。注意: バグ入り。*/
#include <math.h>
#include <string.h>

static double func_floor(double a) { return floor(a); }
static double func_ceil(double a) { return ceil(a); }
static double func_sqrt(double a) { return sqrt(a); }
static double func_sin(double a) { return sin(a); }
static double func_cos(double a) { return cos(a); }
static double func_tan(double a) { return tan(a); }
static double func_exp(double a) { return exp(a); }
static double func_log(double a) { return log(a); }

static const struct
  {
    const char* keyword;
    double (*function)(double x);
  } function_table[] =
  {
      { "ceil", func_ceil },
      { "cos", func_cos },
      { "exp", func_exp },
      { "floor", func_floor },
      { "log", func_log },
      { "sin", func_sin },
      { "sqrt", func_sqrt },
      { "tan", func_tan }
  };

static const unsigned MAX_FUNCTION_INDEX = sizeof function_table / sizeof function_table[0] - 1;

double calc_function(const char* keyword, double a)
  {
    unsigned lo = 0;
    unsigned hi = MAX_FUNCTION_INDEX;
    while (lo < hi)
      {
        unsigned k = (lo + hi) / 2;
        int c = strcmp(function_table[k].keyword, keyword);
        if (c < 0)
            lo = k + 1;
        else if (c > 0)
            hi = k;
        else
            return function_table[k].function(a);
      }
    return 0;
  }

慎重に考えれば calc_function は次のように修正しなければならないことが分かるだろう。

double calc_function(const char* keyword, double a)
  {
    unsigned lo = 0;
    unsigned hi = MAX_FUNCTION_INDEX;
    while (lo <= hi)
      {
        unsigned k = lo + (hi - lo) / 2;
        int c = strcmp(function_table[k].keyword, keyword);
        if (c < 0)
            lo = k + 1;
        else if (c > 0)
          {
            if (k == 0)
                break;
            hi = k - 1;
          }
        else
            return function_table[k].function(a);
      }
    return 0;
  }

とかく境界の判断や更新は慎重に検討すべきである。

車輪を再発明する

「車輪を再発明する愚」という人がよくいるが, 再発明自体は大いによいことである。

ただし, そのための工数を確保するには, 説得力のある理由が必要である。

動作したという理由だけで満足しない

プログラムが動作したからといって完成したと思ってはいけない。

より汎用化できないか考える

汎用的なコードは, 再利用できる可能性が大きいため共通化が進み, プログラムの肥大化を防ぐことに繋がる。しかしより重要な利点は, 汎用的なコードからは特殊な要件が排除されるため, コードをシンプルに保ち, 品質を上げやすいというところにある。

具体的には, アルゴリズムやデータ構造を表現するコードから特殊な業務要件を分離し, パラメータや派生クラスとして実現することを考えるべきである。必要なら Facade を用意し, I/F をシンプルに保つ工夫も役に立つ。

前節で登場したバイナリサーチを汎用化してみよう。

#include <stddef.h>

const void* binary_search(
    const void* key,
    const void* base,
    size_t n,
    size_t size,
    int (*compare)(const void*, const void*))
  {
    size_t lo = 0;
    size_t hi = n - 1;
    while (lo <= hi)
      {
        size_t k = lo + (hi - lo) / 2;
        const void* center = (const char*)base + k * size;
        int c = compare(center, key);
        if (c < 0)
            lo = k + 1;
        else if (c > 0)
          {
            if (k == 0)
                break;
            hi = k - 1;
          }
        else
            return center;
      }
    return NULL;
  }

この仕様は, 標準ライブラリの bsearch という関数ほとんどそのままである。車輪の再発明というわけである。

より簡単な方法がないか考える

動作するコードを書いてみて初めて, 設計フェーズでは気付かなかった抽象化や共通化の方法に気が付くことがある。書いたコードを目視確認していて複数回登場する似たようなパターンに気付いたり, I/F をよりシンプルにできることに気付いたりすることは決して珍しいことではない。

やや作為的だが, ここでは汎用バイナリサーチの仕様を少し変更し, 特定の値以上の最初の位置を返す lower_bound を実装してみる。 こちらの方が応用範囲は広がる。

#include <stddef.h>

const void* lower_bound(
    const void* key,
    const void* base,
    size_t n,
    size_t size,
    int (*compare)(const void*, const void*))
  {
    size_t lo = 0;
    size_t hi = n;
    while (lo < hi)
      {
        size_t k = lo + (hi - lo) / 2;
        if (compare((const char*)base + k * size, key) < 0)
            lo = k + 1;
        else
            hi = k;
      }
    return (const char*)base + lo * size;
  }

バイナリサーチのコードの変数 hi の役割を微妙に変更している。バイナリサーチでは hi は対象範囲の最大インデックスを保持していたが, 今回の lower_bound では最大インデックス+1 を保持している。

また, さらに作為的だがバイナリサーチもループ内が極力短くなるように少し変形してみる。ただし変数 hi の役割は元のままである。

#include <stddef.h>

const void* binary_search(
    const void* key,
    const void* base,
    size_t n,
    size_t size,
    int (*compare)(const void*, const void*))
  {
    size_t lo = 0;
    size_t hi = n - 1;
    while (lo < hi)
      {
        size_t k = lo + (hi - lo) / 2;
        if (compare((const char*)base + k * size, key) < 0)
            lo = k + 1;
        else
            hi = k;
      }
    return compare((const char*)base + lo * size, key) == 0 ? (const char*)base + lo * size : NULL;
  }

こう変形してみると, 変数 hi の役割が違っているにもかかわらず, ループ部分のコードは lower_bound と全く同じコードとなった。実際にコードを書いてみる前にこのような事実に気付くのは難しい。

より効率化できないか考える

変数 lo, hi で対象範囲を絞込んでいくのではなく, 変数 lo と区間幅 n で対象範囲を絞り込んでいくことを考え, この 2 変数の動きを慎重に検討すると, else 節をなくすことができることが分かる。さらに変数 base と n で制御すればより簡単になるが JAVA ではできない。

#include <stddef.h>

const void* lower_bound(
    const void* key,
    const void* base,
    size_t n,
    size_t size,
    int (*compare)(const void*, const void*))
  {
    size_t lo = 0;
    while (n > 0)
      {
        size_t k = n / 2;
        if (compare((const char*)base + (lo + k) * size, key) < 0)
          {
            lo += k + 1;
            --n;
          }
        n /= 2;
      }
    return (const char*)base + lo * size;
  }

乗算は, 加減算と比べると効率が悪いことが多い。工夫するとコードは長くなるがループ内から乗算を追い出すことができる。ここから先は C 専用の最適化を行っている。

#include <stddef.h>

const void* lower_bound(
    const void* key,
    const void* base,
    size_t n,
    size_t size,
    int (*compare)(const void*, const void*))
  {
    size_t d = n * size;
    while (n > 0)
      {
        if (n % 2 == 0)
          {
            const char* p = (const char*)base + (d /= 2);
            if (compare(p, key) < 0)
              {
                base = p + size;
                d -= size;
                --n;
              }
          }
        else
          {
            const char* p = (const char*)base + (d = (d - size) / 2);
            if (compare(p, key) < 0)
                base = p + size;
          }
        n /= 2;
      }
    return base;
  }

さらに魂をこめる。(x & -x) という式で x のビットパターン中の最下位の 1 だけを取出すことができる。このことを利用すると, 上のコードの変数 n を省略することができる。

#include <stddef.h>

const void* lower_bound(
    const void* key,
    const void* base,
    size_t n,
    size_t size,
    int (*compare)(const void*, const void*))
  {
    size_t odd_bit = size & -size;
    n *= size;
    while (n > 0)
      {
        if ((n & odd_bit) == 0)
          {
            const char* p = (const char*)base + (n /= 2);
            if (compare(p, key) < 0)
              {
                base = p + size;
                n -= size;
              }
          }
        else
          {
            const char* p = (const char*)base + (n = (n - size) / 2);
            if (compare(p, key) < 0)
                base = p + size;
          }
      }
    return base;
  }

ややマニアックな話題だっただろうか。

主義主張を持つ

プログラミングスタイルを確立する

自分なりのプログラミングスタイルを確立するのは重要である。自分のスタイルで美しいコードには愛着が沸く。何より一定のポリシーに従ったプログラムコードは読みやすい。

プログラミング哲学に触れる

R. C. Martin らによってオブジェクト指向設計の原則がまとめられている。特に目新しい哲学とは言えないが, 設計の様々な段階でチェックリストとして用いると便利である。

原則名 英語名 略記 意味
リスコフの置換原則 The Liskov Substitution Principle LSP 派生型はその基本型と置換可能でなければならない。
依存の非循環原則 The Acyclic Dependencies Principle ADP パッケージは循環依存してはならない。
依存関係逆転の原則 The Dependency Inversion Principle DIP 上位レベルモジュールは下位レベルモジュールに依存してはならない。どちらも抽象モジュールに依存する方がよい。抽象は詳細に依存してはならない。
I/F分離の原則 The Interface Segregation Principle ISP インターフェースは最小であるべき。クライアントが利用しないメソッドへの依存を強制してはならない。
安定依存の原則 The Stable Dependencies Principle SDP パッケージの依存方向は安定性の方向と一致すべきである。
安定抽象の原則 The Stable Abstraction Principle SAP 安定したパッケージは抽象的であるべきである。
開放/閉鎖の原則 The Open-Closed Principle OCP ソフトウェアの構成要素 (クラス、モジュール、関数など) は拡張に対して開いていなければならず、修正に対しては閉じていなければならない。
全再利用の原則 The Common Reuse Principle CRP 1パッケージ内のクラス群は同時に再利用される。
リリース-再利用等価の原則 The Release-Reuse Equivalency Principle REP 再利用の粒度はリリースの粒度と一致する。
閉鎖性共通の原則 The Common Closure Principle CCP 1パッケージ内のクラス群は同種の変更に対して閉じている。パッケージに影響する変更はパッケージ内のすべてのクラスに影響を及ぼすが、ほかのパッケージには影響しない。
単一責務の原則 The Single Responsibility Principle SRP 1つのクラスに1つの責務。クラスを変更する理由は1つ以上存在してはならない。(責務=変更理由)

魂を抜く

「一行入魂」を実践することはそれほど難しいことではないが, これが「万行入魂」とかになってくると, 必ずしもすべての行に同じ集中力で魂を込めるというのは現実的でなくなってくる。

イディオムを使いこなす

たとえば多くのプログラマは C++ や JAVA で n 回だけ繰返したいと思えば

        ...
        for (int k = 0; k < n; ++k)
            ...

というコードが特に考えなくても出てくるだろう。C/C++ でリスト構造を先頭から末尾まで辿りながら処理したいと思えば

        ...
        for (p = head; p != NULL; p = p->next)
            ...

である。この程度のコードならほとんど反射的にタイプする人もいる。

私の場合, その昔 i8080 系の CPU の全盛期に, アセンブリ言語で大量にコードを書いた経験があるが, このテの CPU はアドレッシングが貧弱で, 小さなテーブルルックアップだけでも次のようなコードを必要とした。しかし, この一連の流れはもう指が覚えてしまっていて, 最初のステップをタイプし始めながら, 6 ステップ先以降のコードを考えるというような芸当をこなしていた。高級言語の 1 ステップが数ステップから数十ステップにも膨らむアセンブリ言語で大量のコードを書くには, このような熟練はほとんど必須と言える。

        LHLD    base
        LDA     index
        MOV     E, A
        MVI     D, 0
        DAD     DE
        MOV     A, M

いや, 今はもうほとんど覚えていない。

もちろんイディオムを構成する一連のコードは, 一度は魂を込めて考える必要がある。いわば一度込めた魂を再利用しているわけである。

メリハリを意識する

矛盾することを言うようだが, プログラム全体の中には密度が薄く, 再利用可能性をほとんど考える余地のないコードが大量に含まれることになる。魂を削って心血注ぐべきコードの割合が大きくなるようでは, あまりバランスのよいプログラム設計とは言えない。

設計書を妄信しない

設計者も間違う

信用しないと言うと語弊があるが, プログラマが渡された設計書は絶対的なものではないし, まして完全なものだと信じてはいけない。自分で納得できないプログラムは書くべきではない。プログラマはキーパンチャではないのだから。

設計へのフィードバック

私は昔物理学を勉強していたのだが, 物理学の根底には, 「正しい理論はシンプルであり, 数学的にも美しい理論である」という漠然とした信念がある。やたら前提条件が複雑であったり, 表現するのに複雑な数式が大量に必要であったりすると, その理論は別の大きな理論の一部を歪に切り取ったものなのではないか, とか, その理論をシンプルに表現できる数学が発明されるべきだと考えるわけである。

プログラムコードについても似たような考え方が成り立たなくもない。ある単純な処理を行うコードがむやみに複雑だったりした場合, 処理仕様が歪んでいるのではないのかとか, プログラミング言語や標準ライブラリに何か問題があるのではないかと疑ってみるのだ。

前者の場合, 処理仕様の改訂が提案できるような状況では大いに提案すべきである。

後者の場合, プログラミング言語の選定をやり直すというのは状況的に受入れられないことが多いが, たとえば非 OOP 言語で OOP の真似をする規約を提案するなど, 工夫によっては何とかなる可能性もある。

推薦図書

書名 著者 内容
プログラミング言語C Brian W. Kernighan
Dennis M. Ritchie
C言語のバイブル。コード例の質が高く, C言語に興味がなくても為になる。著者の頭文字をとって「K&R」とよく省略される。
プログラミング言語C++ Bjarne Stroustrup C++言語のバイブル。分量も密度も高く, 示唆に富んでいる。
プログラム書法 Brian W. Kernighan
P. J. Plauger
古典的な名著。良いプログラムを書くためのべからず集。
文芸的プログラミング Donald E. Knuth プログラムは文芸作品であるという哲学を実践する本。Javadoc にも影響を与えている。
ハッカーの楽しみ
―本物のプログラマはいかにして問題を解くか
Henry S. Warren Jr. さまざまなプログラミングテクニック集。内容は非常に濃い。
達人プログラマー
―システム開発の職人から名匠への道
Andrew Hunt
David Thomas
良いソフトウェアを開発/設計/製造するための指針。
Javaによるアルゴリズム事典 奥村晴彦 アルゴリズムの事典。「Javaによる」という部分には期待しない方がよい。解説は薄いため, 本格的な勉強用には向かない。

.NET データベースプログラミング

ADO.NETの基本

1. ADOとADO.NET

ADO とは,“ActiveX Data Object” の略であり,.NET 以前にデータベースにアクセスするための技術でした。ADO.NET は,この ADO をさらに進化させたものとなっています。

2. 構成

ADO.NET を構成するクラス群は,大きく2種類の部分に分けることができます。一つのグループは実際にデータベースシステムを操作するためのクラス群であり,もう一つのグループはデータを扱うためのモデルクラス群であるということができます。

前者はデータプロバイダと呼ばれていて,利用するデータベースシステムやドライバに応じていくつかのクラス群を選択して使用することになります。

後者は特に名前が付けられていないようですが,このクラス群の代表的クラスの DataSet の名前で呼ばれることがあるようです。この DataSet クラス群が,直接データベースに依存していない,つまりデータプロバイダと完全に分離されていることが,ADO.NET の大きな特徴となっています。この構成は非接続型アーキテクチャという言葉で呼ばれています。

次の図は,データプロバイダと DataSet の構造の概要を示しています。

上記以外にも,NET Framework の多くの GUI 部品は,ADO.NET と接続して簡単に GUI を構築するための機能を持っています。

3. データプロバイダ

標準のデータプロバイダには,以下の4種類があります。

・.NET Framework Data Provider for SQL Server

Microsoft SQL ServerのVersion 7.0 以降で利用するためのデータプロバイダです。このデータプロバイダは直接データベースシステムにアクセスするため,軽量かつ高速であるとされています。

名前空間は System.Data.SqlClient となっています。

・.NET Framework Data Provider for Oracle

Oracle クライアントを通じて Oracle にアクセスするためのデータプロバイダです。これは .NET Framework 1.1 になってから導入されました。

Oracle 自身も独自のデータプロバイダを配布しているため,Oracle を利用する場合はどちらかを選択することになります。

名前空間は System.Data.OracleClient となっています。

・NET Framework Data Provider for OLE DB

以前から利用されていた OLE DB を利用するためのデータプロバイダです。要するに OLE DB ブリッジです。OLE DB を経由して間接的にデータベースシステムにアクセスするため,上記のデータプロバイダと比べると効率が悪くなります。

名前空間は System.Data.OleDB です。

・.NET Framework Data Provider for ODBC

ODBC 経由でデータベースにアクセスするためのデータプロバイダです。.NET Framework 1.1 から標準となっています。

名前空間は System.Data.Odbc ですが,.NET Framework 1.0 で Microsoft の配布サイトからダウンロードして利用する場合は Microsoft.Data.Odbc となります。

4. 基本的なコード例

(1) 単純な SQL の実行

早速,SQL Server を例に,C# の簡単なコードを書いてみます。

 1: using System;
 2: using System.Data.SqlClient;
 3:
 4: public class SanCorporation
 5: {
 6:    public static void Main()
 7:    {
 8:        using (SqlConnection connection =
 9:               new SqlConnection("Data Source=.\\SQLEXPRESS; " +
10:                                 "Integrated Security=True; " +
11:                                 "Database=San"))
12:        {
13:            connection.Open();
14:            SqlCommand command =
15:                new SqlCommand("UPDATE bonus SET amount = amount * 2",
16:                               connection);
17:            command.ExecuteNonQuery();
18:        }
19:    }
20: }

DB 接続 (connection) の扱い方は C# の using 構文を使うと面倒が少なくてすみます (8~12, 18行)。SQL の実行は,コマンド (command) の生成と ExecuteNonQuery メソッドの実行のたった2ステップで完了してしまいます。

このようにパラメータを持たない,更新系の SQL は非常に簡単ですが,実際にはパラメータの定義とバインド,検索系であれば,取得結果の読み取りなど,もう少し複雑になるのが普通です。

(2) パラメータつき SQL の実行

次は,パラメータの例です。

 1: using System;
 2: using System.Data.SqlClient;
 3:
 4: public class SanCorporation
 5: {
 6:     public static void Main()
 7:     {
 8:         using (SqlConnection connection =
 9:                new SqlConnection("Data Source=.\\SQLEXPRESS; " +
10:                                  "Integrated Security=True; " +
11:                                  "Database=San"))
12:         {
13:             connection.Open();
14:             SqlCommand command =
15:                 new SqlCommand("UPDATE bonus SET amount = amount * @rate " +
16:                                "WHERE rank >= @rank",
17:                                connection);
18:             command.Parameters.Add("@rate", 3);  // .NET 2.0 では Add → AddWithValue
19:             command.Parameters.Add("@rank", 7);  // .NET 2.0 では Add → AddWithValue
20:             command.ExecuteNonQuery();
21:         }
22:     }
23: }

SQL に @変数名 の形でパラメータを埋め込んでおき,後に ParametersCollection を通じて値をセットしています (18~19行)。

(3) DataReader による結果の取得

検索系の SQL を発行し,データベースからデータを取得する方法として,大きく DataReader を用いて読み取る方法と,DataSet を用いる方法の二通りがあります。まずは DataReader を用いて取得する例です。

 1: using System;
 2: using System.Data.SqlClient;
 3:
 4: public class SanCorporation
 5: {
 6:     public static void Main()
 7:     {
 8:         using (SqlConnection connection =
 9:                new SqlConnection("Data Source=.\\SQLEXPRESS; " +
10:                                  "Integrated Security=True; " +
11:                                  "Database=San"))
12:         {
13:             connection.Open();
14:             SqlCommand command =
15:                 new SqlCommand("SELECT * FROM bonus WHERE amount >= 50000 " +
16:                                "ORDER BY amount DESC",
17:                                connection);
18:             SqlDataReader reader = command.ExecuteReader();
19:             while (reader.Read())
20:             {
21:                 Console.WriteLine("id=" + reader["id"] + ", " +
22:                                   "amount=" + reader["amount"]);
23:             }
24:             reader.Close();
25:         }
26:     }
27: }

上記例のように,DataReader を用いてデータを取得するには,コマンドの ExecuteReader メソッドによって SQL を発行します (18行)。また,Read メソッドによって1行1行読み進めながら,インデクサを用いて個々の値を取得することができます。

(4) DataSet による結果の取得

DataSet とデータベースの間でデータをやり取りするには,DataAdapter を用います。DataAdapter は,DataReader を使用して結果を読取り,DataSet にデータを充填するという処理を行います。

DataSet にも大きく2種類の使い方があります。ひとつは DataSet をそのまま利用する方法で,もうひとつは DataSet から専用のクラスを派生させて利用する方法です。前者を「型指定されていない DataSet」,後者を「型指定された DataSet」と呼びます。

型指定された DataSet は DataSet を派生した専用クラスですが,実際にプログラマがコーディングする必要はなく,Visual Studio のウィザードで簡単に作成することができるようになっています。今回は,もっぱら型指定されていない DataSet を使用しますが,コード例は後ほど示します。

(5) データプロバイダ依存のコーディングについて

ADO.NET のサンプルプログラムは,このセミナーで提示しているものも含めて,データプロバイダに依存するコードを多用しているものがほとんどです。が,少しの手間を惜しまなければ,汎用性を持たせることも可能です。そのためには,SqlCommand などの具象クラス名の代わりに System.Data 名前空間に用意されている,IDbCommand などのインターフェース名を使用してプログラムを記述します。ただし,.NET 2.0 以前はファクトリが用意されていなかったので,そこだけはプログラマが Abstract Factory パターンを応用するなどして何とかしなければなりませんでした。この欠点は .NET 2.0 では解消されています。(System.Data.DbProviderFactoryクラス)

ただし,プログラムコードを共通化しても,SQL やパラメータ名などの制約がデータベースシステム毎に異なりますので,結局は何らかの依存性が残ることになります。

・トランザクション

同一のデータを複数のプログラムあるいはオペレータが無秩序に操作すれば,さまざまな不都合が発生します。また,ある一連の操作を実行中にシステムがダウンした場合,データが矛盾した状態になってしまうことがあります。このような不都合が発生しないようにするためには,他のプログラムの影響を受けたくない一連の独立した処理単位を定義し,その実行中は他のプログラムを待ち合わせたり,いつシステムダウンしても,元に戻せるようなバックアップを残しておくなどの制御が必要となります。この独立した処理単位のことをトランザクションと呼びます。

たとえば,ある銀行の当座預金から普通預金に 100万円を移し変えるという処理を考えて見ます。この処理と前後して,ちょうど会社から給与20万円の振込の処理も発生したとしましょう。これを何の工夫もなしに行ってしまうと,たとえば次のような順序で処理が進むかも知れません。何が起こるか考えてみてください。

順序 当座→普通処理 給与振込処理
1 当座預金残高をAに読込む  
2 Aが100万以上かチェック  
3 Aから100万円を減算  
4   普通預金残高をXに読込む
5 Aを当座預金残高に書込む  
6   Xに振込金額20万円を加算
7 普通預金残高をBに読込む  
8   Xを普通預金残高に書込む
9 Bに100万円を加算  
10 Bを普通預金残高に書込む  

1. トランザクションとは

(1) ACID特性

トランザクションは,次の4つの特性を満たさなければならないとされています。

・原子性 (Atomicity)

トランザクションは,分割することができない処理単位です。トランザクションが成功したとき,含まれるすべての処理が成功したことになりますし,トランザクションが失敗したときは,すべての処理が一切行われていない状態になっています。途中まで成功した状態でトランザクションが終わることはできません。

・一貫性 (Consistency)

トランザクションの実行前も実行後も,データの整合性を保っていなければなりません。トランザクションの実行の前後で矛盾があってはならないということです。

・隔離性 (Isolation)

トランザクションは互いに他のトランザクションに影響を与えないということです。前述の例のようなことがあってはならないということです。

・持続性 (Durability)

トランザクションの結果は保存されなければなりません。トランザクションの完了後,いつのまにかトランザクションが取消されているなどということがあってはならないということです。

(2) ローカルトランザクションとグローバルトランザクション

通常,データベースシステムには自分自身の管理するデータに対するトランザクションを保証する仕組みを備えています。このような自分だけに限定したトランザクションのことをローカルトランザクションと呼ぶことがあります。

それに対して,たとえば銀行口座から引出す処理と,発注テーブルにレコードを挿入する処理を不可分に行いたいというような場合,複数のシステムにまたがったトランザクションが必要となります。このようなトランザクションをグローバルトランザクションと呼びます。

(3) プログラマティックなトランザクションと宣言的トランザクション

明示的トランザクションと暗黙的なトランザクションという言い方をすることもあります。

・プログラマティックなトランザクション

トランザクション制御の開始と終了をプログラムの処理として実行するという意味です。実際にコーディングするときは C# の using キーワードが役に立ちます。

・宣言的トランザクション

たとえば,このメソッドはトランザクションとして実行する,というような宣言に基づいて制御されるトランザクションのことです。トランザクションの設計からシームレスに実装に繋がるし,何よりトランザクションの終了し忘れなどを未然に防止することができるというメリットがあります。

2. データプロバイダのトランザクション

たとえば .NET Framework Data Provider for SQL Server の場合,SqlTransaction というクラスが用意されています。このオブジェクトを用いてトランザクション制御を行うことができますが,この場合,データベースシステム (SQL Server) のローカルトランザクションをプログラマティックに制御することになります。

3. EnterpriseServices (COM+)

.NET 以前から Windows には COM+ という仕組みが用意されており,VisualBasic などで宣言的トランザクションを利用することができました。.NET でもこの仕組みを利用することができ,「属性」でトランザクションを宣言することが可能です。

この方式には欠点もいくつかあります。

・この仕組みは MS DTC (Microsoft Distributed Transaction Coordinator) を利用しており,不必要に分散トランザクションが発生することがあります。このため,効率が低下しがちです。

・COM+ サービスを利用するためには,コンポーネント化が必須です。アセンブリを分離した上で,署名する必要があります。

ターゲットが Windows 2003 Server の場合は後者の欠点は克服することができます。Windows 2003 Server では,アプリケーションプログラムの一部をコンポーネントとして実行するという機能が使えます。この機能は System.EnterpriseServices.ServiceDomain クラスで制御することができます。ただし,この場合「宣言的なトランザクション」とは言えなくなります。

4. TransactionScope

.NET 2.0 の新機能として追加された仕組みです。この機能は,プログラマティックなトランザクションと宣言的トランザクションの中間的な制御,いわば宣言的トランザクションをプログラマティックに制御する機能となっています。また,ローカルトランザクションと分散トランザクションを適切に自動的に切り替える機能を持っています。

.NET 2.0 以降では,この機能以外は忘れてしまって構わないのではないかと思います。

トランザクションのスコープとして,次の3種類が用意されています。

・Required … このスコープには,トランザクションが必要です。アンビエント トランザクションが既に存在する場合は,アンビエント トランザクションを使用します。それ以外の場合は,スコープに入る前に新しいトランザクションが作成されます。これは既定値です。

・RequiresNew … スコープに対して,常に新しいトランザクションが作成されます。

・Suppress … スコープ内では,アンビエント トランザクションのコンテキストは抑制されます。スコープ内のすべての操作は,アンビエント トランザクションのコンテキストを使用せずに行われます。

エンタープライズアプリケーション・アーキテクチャーパターン入門

 はじめに

 本資料は、株式会社アクシオン・ソフトウェアが主催するSAN(SaturdayActiveNetwork)で行う、勉強会用の資料である。 「エンタープライズアプリケーションアーキテクチャ入門」の書籍から、勉強会用にピックアップしたものになります。

対象読者としては、ビジネスアプリケーションに携わるプログラマ、設計者、およびアーキテクトとしている。 また、JavaC#の知識があることが必要である。

本資料では、開発者が早期に確認したいと思っている決定事項やアプリケーションの主要となるパーツについて、パターンを通じて説明する。 また、エンタープライズアプリケーションとしているため、給与計算・診療記録・出荷管理・会計・顧客サービスなどのアプリケーションを対象とする。

アプリケーションアーキテクチャ

アーキテクチャは、システムを開発・存続していく上で、重要な要素である。 しかし、なかなか決定できない要素でもある。 アーキテクチャには少なくとも2つの要素があると考えられる。 1つは、システムから個々のパーツへブレークダウンできること、1つは、簡単には変更できない決定事項であるということである。 また、1つのシステムには、複数のアーキテクチャが存在してもよく、システムの存続期間中に変わることもありえる。

レイヤ化

レイヤ化は、ソフトウェアシステムの分割のために使用する一般的な技法である。

以下のようなメリットがある。

      各レイヤは、その下位のレイヤについてのみ知っていればよく、さらにその下位のことは知らなくてもソフトウェアが構築できるのである。 たとえば、イーサネットの動作を知らなくても、TCPの上のFTPサービスを構築できる。

      レイヤを入れ替えることも可能となり、柔軟なシステムを構築することができる。 たとえば、FTPサービスは、イーサネット・PPP・ケーブル会社が変わっても、実行に支障はない。

また、デメリットもある。

レイヤを追加するとパフォーマンスを損ねる可能性がある。

 

        

 

1.        3層アプリケーションアーキテクチャ

 これまでのクライアント/サーバシステムでは、ファットなクライアントがUIおよびコントロールをつかさどり、データベースサーバが存在する形であった。 Webの発展に伴い、UIはブラウザで行うことが主流となり、また、オブジェクト指向の普及に伴って、3層のアーキテクチャが注目されるようになった。 ここでの3層とは、プレゼンテーションレイヤ(UI)・ドメインレイヤ・データソースレイヤとなる。 プレゼンテーションレイヤでは、画面の入力・表示を主体とし、ドメインレイヤでは、そのシステムのビジネスロジックを担当する。 データソースレイヤは、DBMSまたはメッセージングシステム、トランザクションモニタ、ファイルシステム、その他のパッケージとの通信などを行う。 プレゼンテーションレイヤとドメインレイヤが分離したことで、プレゼンテーションレイヤに依存しないドメインレイヤの構築が可能となった。 また、クライアント/サーバシステムでは、必然的に、クライアントとサーバの構成をとることになるが、3層のアーキテクチャでは、クライアントであるブラウザとサーバサイドのプレゼンテーションレイヤが分離されるほか、DCOMJ2EEWebサービスなどの普及にも伴い、ドメインレイヤの物理的な配置にも自由度があがっている。

図1を参照。

 

 


1 レイヤと物理的配置

 

 



2.        ドメインレイヤ

 ドメインレイヤの構築について、3つの主要なパターンに分類して考える。

 

 

2.1.       トランザクションスクリプト

 プレゼンテーションレイヤからの入力に従い、妥当性検証・計算やデータベースアクセス、他システムの操作の呼び出しなどを行い多くのデータを伴ってプレゼンテーションレイヤに応答する。 

 

 トランザクションスクリプトには、以下の長所がある。

      開発者の大半が理解できる手続き型のモデルである。

      トランザクションの境界がはっきりしている。 処理の開始と終了がその境界となる。

      後に述べる行データゲートウェイやテーブルデータゲートウェイなどのシンプルなデータソースレイヤと連結する。

 また、短所を挙げる。

      同じような機能では、コードが重複することが多い。

      上述より保守性にかける。 これは、開発者が増えれば増えるほど、保守費用の増大を招く。


 

 

 

 

 



2.2.       ドメインモデル

 主にドメイン内(ビジネス内)の名詞について体系化したモデル。

たとえば、賃貸システムなら、賃貸借契約、資産などのクラス分けを行い、妥当性確認や計算のロジックは各クラスに配置する。

 

 ドメインモデルには、以下の長所がある。

      ドメインに変更や追加があったときに、対象オブジェクトの変更や追加を行うことで対応可能となる。

      ドメインモデルになれてくれば、複雑なロジックでも体系化された多くの技法(継承やストラテジ、デザインパターンなど)が存在する。

      関連する複数のドメインから、利用することができる。

 また、短所を挙げる。

      手続き型からのパラダイムシフトとなり、慣れるまでに時間を要する。

 


 

 

 

 



2.3.       テーブルモジュール

 データベーステーブルやビューのレコードセットに関するビジネスロジックを扱うひとつのインスタンス。

 ドメインモデルでは、契約ごとにインスタンスがあるのに対して、テーブルモジュールでは、1つのインスタンスしかない。 クライアントは、契約に関する操作を呼び出していろいろなことが行える。 個別の契約に対しての操作では契約IDを渡さなければならない。

 テーブルモジュールは、さまざまな意味でトランザクションスクリプトとドメインモデルの中間に位置する。

 

テーブルモジュールの長所を述べる。

      直線的な手続きではなく、テーブルを中心にドメインロジックを構築することで、構造がわかりやすく、重複の発見と削除が容易になる。

      多くのGUI環境は、レコードセットにまとめられたSQLクエリの結果にしたがって動作する。テーブルモジュールは、レコードセット上で動作するため、容易に処理できる。

      MicrosoftCOM.NETは、この開発スタイルを採用している。

また、短所を挙げる。

      ドメインロジックが持つ多くの技法(継承やストラテジ、デザインパターンなど)は使用できない。

      大きなレコードセットを扱ってしまうことがある。

 


 



      選択

 3つのパターンからどうやって選ぶか。 主に、ドメインロジックの複雑度によって決まることと考えられる。

 ドメインモデルはシンプルなドメインロジックに対しては魅力的ではない。 しかし、他の手法では、ドメインロジックが複雑になると飛躍的に難しくなる傾向がある。

 図に、科学的ではないが3つのパターンに関するドメインロジックの複雑性と労力の関係を表す。


 

 


 ドメインモデルに習熟したチームであれば、初期コストがかからないため、ドメインモデルを採用することが望ましいと考えられる。 テーブルモジュールの魅力は、共通のレコードセット構造を環境がサポートしているかどうかで異なる。 レコードセット用の多くのツールが存在する.NETの場合、とても魅力的である。

 これら3つのパターンは、相互に排他的な選択肢ではない。 ドメインロジックの一部にトランザクションスクリプトを使用し、それ以外にテーブルモジュールまたはドメインモデルを使用することは珍しくない。

 


3.        サービスレイヤ

 ドメインレイヤを2つに分割する共通の手法がある。 サービスレイヤは、ドメインモデルまたはテーブルモジュールの上に配置される手法である。 プレゼンテーションロジックは、アプリケーションのAPIの役割を果たすサービスレイヤだけを通してドメインとやり取りする。

 サービスレイヤは、明確なAPIを提供するだけでなく、トランザクション制御やセキュリティの確保のための最適な場所となりうる。 .NETでは属性で直接記述することができる。

 振る舞いに関して2つのパターンが存在する。

サービスレイヤをファサードとするパターンでは、実際の振る舞いはすべて下位のオブジェクトにあり、サービスレイヤは、下位のオブジェクトに呼び出しを転送する。トランザクションラッパーやセキュリティチェックなどを追加するのに便利である。

 次に、サービスレイヤ内に、トランザクションスクリプトを置き、シンプルなドメインモデルを呼び出すパターンがある。 ドメインモデルがシンプルなためデータベースと11の関係になり、シンプルなデータソースレイヤを使用することができる。

 

 


4.        リレーショナルデータベースへのマッピング

 現在のシステムの大半においてデータベースとはリレーショナルデータベースを指している。 このため、ドメインロジックがリレーショナルデータベースとやり取りするためのアーキテクチャに関するパターンを説明する。

 


4.1.       ゲートウェイ

 SQLをプログラミング言語に組み込む手法はあるが、チューニングやインデックスの配置などにかかわるトラブルが発生する。 このため、ドメインロジックとSQLアクセスを分離し、異なるクラスに配置することは懸命である。

 これらのクラスを組織化する適切な方法は、データベースの構造に基づいて、テーブル毎に1つのクラスを持つように設計することである。 これらのクラスはそのテーブルに対するゲートウェイを形成する。 これにより、アプリケーションはSQLについて知る必要はなく、SQLはすべて容易に見つけられる。

 

4.2.       行データゲートウェイ

 クエリから返された行ごとにインスタンスを持つオブジェクトである。 オブジェクト指向の考え方に自然に適合する手法である。


 

 


4.3.       テーブルデータゲートウェイ


 レコードセットを持つゲートウェイ。 テーブルデータゲートウェイは、データベースを検索し、レコードセットを返すメソッドを持つ。 また、ストアドプロシージャのコレクションをテーブル用のテーブルデータゲートウェイと考えることもできる。 この場合、テーブルデータゲートウェイは、ストアドプロシージャの呼び出しをラップする。

 

 

 

 



4.4.       アクティブレコード

テーブルまたは行をラップし、データベースアクセスをカプセル化してデータにドメインロジックを追加するオブジェクト。 シンプルなドメインロジックでは、この手法は有効であ る。

 

 

 

 

 

 

 

 

 ドメインロジックが複雑化し、ドメインモデルに移行する場合、アクティブレコードでは対応しづらくなってくる。 

ドメインモデルが豊富になると、間接化が強要されてくる。 ゲートウェイで一部解決はできるが、結果としてゲートウェイのフィールドからドメインモデルのフィールドに変換されるため、ドメインオブジェクトが複雑化してくる。

これに対応する手法は、ドメインオブジェクトとテーブルとのマッピングを間接参照のレイヤに行わせ、データベースからドメインモデルを完全に分離させる方式である。

 

 

4.5.       データマッパー

データマッパーは、データベースのドメインモデル間の読み込みと保存のすべてに対処し、両方の変更を独立して行うことができる。 

 

 

 

 

 

 


データマッパーにより、ドメインロジックはデータベースが存在するかさえも知る必要はなく、SQLを使用したり、データベーススキーマの知識も必要ない。(データベーススキーマは使用するオブジェクトについて何も知らない。)

挿入や更新を行う場合、データベースマッピングレイヤは、どのオブジェクトが変化し、どのオブジェクトが新たに作成され、また、どのオブジェクトが削除されたかを知っておく必要がある。 この作業のため、ユニットオブワークパターンを後述する。

 


4.6.       振る舞いに関する問題

振る舞いに関する問題は、さまざまなオブジェクトをデータベースに読み込ませる方法と保存させる方法である。

アクティブレコードでは、読み込みと保存のメソッドを持つ。

メモリにオブジェクト群を読み込んで修正する場合、修正したオブジェクトを記録し、それらすべてをデータベースに書き戻さなければならない。 2,3のレコードを読み込むだけであれば、これは、容易である。 多くのオブジェクトを読み込むほど行うことが多くなる。 これは特に、行の作成や修正を行う場合に、参照する行を修正する前に作成された行のキーが必要になることなどからである。

オブジェクトを読み込んで修正する場合は、処理するデータベースが一貫性のある状態であるようにしなければならない。 他のプロセス・スレッドが同じオブジェクトを読み込んで変更することがないように分離する必要がある。 これについては、並行性の問題で後述する。

これらの問題の解決策としてユニットオブワークがある。 ユニットオブワークは、少しでも修正されたすべてのオブジェクトとともに、データベースから読み込まれたすべてのオブジェクトを記録する。 また、データベースの更新方法にも対処する。 プログラマは明示的に保存メソッドを呼び出すアプリケーションプログラムの変わりに、コミットする処理単位を設定する。 そして、この処理単位はすべてのデータベースに対する適切な振る舞いを順序付け、1箇所にすべての複雑なコミット処理を配置する。

オブジェクトを読み込む際は、同じオブジェクトを2度読み込まないように注意が必要である。 2度読み込むと1つのデータベース行に対応するオブジェクトが2つ存在することになる。 両方を更新すると問題が発生する。 これに対応するために一意マッピングで読み込んだ各行の記録を保持する必要がある。 データを読み込む際に、最初に一意マッピングをチェックし、読み込まれている場合は、そのデータに対する2つ目の参照を返すことができ、このことにより、更新も適切に調整される。

ドメインモデルを使用する場合、注文オブジェクトの読み込みが関連する顧客オブジェクトを読み込むというようにリンクされたオブジェクトが一緒に読み込まれるように配置することが多い。 しかし、多くのオブジェクトが連結している場合、膨大なオブジェクトをデータベースから抜き出すことになる。 この無駄を回避するため、抽出データは減らすが、その後の必要に応じて、データを抜き出すためのドアを開けておく。 レイジーロード(後述)は、オブジェクト参照用のプレースホルダーがあることを期待する。 レイジーロードでは、実際のオブジェクトを指す代わりに、プレースホルダーであるというマークをつけておき、リンクをたどろうとするとき、実際のオブジェクトがデータベースから呼び出される。

 

 


4.7.       構造的なマッピングに関するパターン

 

関係のマッピング

ここでの問題は、オブジェクトとリレーショナルデータベースのリンクに対処する方法の違いに関する違いであり、2つの問題がある。 1つは、表現の違いであり、オブジェクトがメモリ管理環境か実行時のメモリアドレスによって保持される参照を格納することでリンクに対処しているが、リレーショナルデータベースは、他のテーブルにキーを形成することでリンクに対処していることである。 もう1つは、オブジェクトが1つのフィールドから複数の参照を処理するために容易にコレクションを使用できるのに対し、正規化はすべての関連リンクを単一の値にすることを強要する。 注文オブジェクトは注文を参照する必要のない明細オブジェクトを自然に持っているが、テーブル構造はその逆で、注文は複数値フィールドを持つことができず、明細が注文への外部キーの参照を持たなければならない。

この表現の問題に対する対処法は、各オブジェクトのリレーショナルな一意性を一意フィールドとしてオブジェクトに保持し、オブジェクト参照とリレーショナルキーとの間を双方向にマッピングするために、これらの値を参照することである。

 

 

 

4.8.       メタデータの使用

 シンプルで繰り返されるマッピングを使用すると、シンプルで繰り返されるコードを作ることにつながる。 同じコードの繰り返しがあるということは設計に間違いがあるといえる。

 共通の振る舞いを継承や委譲で処理するようにすることで多くのことが行える。(これは、有効で正当なオブジェクト指向のプラクティスといえる)

 メタデータマッピングは、データベースの列をオブジェクトのフィールドにマッピングする方法を記述したメタファイルへのマッピングを集約することに基づいている。 メタデータにより、コードの生成、あるいは自己反映的なプログラミングのどちらかを使用して同じコードの繰り返しを回避することができる。

 

 <field name=”customer” targetClass=”Customer” dbColumn=”custID” targetTable=”Customers” lowerBound=”1” upperBound=”1” setter=”loadCustomer”/>

 

 このメタデータから読み込みと書き込みの定義、任意のジョインの自動生成、すべてのSQLの実行、関係の多重度の強制、参照整合性がある場合における書き込み順序の認識など、さまざまな実行が行える。

 

 


4.9.       データベース接続

 多くの環境では、接続の作成にかかるコストは高いので、接続プールを作成するほうがよい。 開発者は接続の作成と終了を行うのではなく、プールから接続を要求し、終了時に解放する。 さまざまなプラットフォームでプールが提供されているため、自分で作成することは稀である。 自分で作成する場合はパフォーマンス測定を行ったほうがよい。

 プールを提供する環境では、新しい接続を作成するようなインターフェースの裏側にプールを置くことが多い。 このため、最新の接続なのか、プールから割り当てられたのかはカプセル化される。

 接続はトランザクションと強く結びついているので、トランザクションと接続(プール)を関連つけて管理する方法がよい。 ユニットオブワークによって、自然と管理できる。

 

 

 

 

 

 


5.        Webプレゼンテーション

 

モデルビューコントローラ(MVC


モデルを完全にWebプレゼンテーションから分離できるメリットを持つ。(これは、プレゼンテーションレイヤとドメインレイヤの分離を意味する。) 現在では、一般的にこの手法を用いることが多い。 図にシーケンス図を示す。

 

 


アプリケーションコントローラ

 Strutsなどのフレームワークが想定されるレイヤ。 Strutsでは、画面に対するアクション(ドメイン)・画面遷移・リクエストパラメータ・パラメータ検証などをコントロールすることが可能である。 これらの機能により、プレゼンテーションレイヤとドメインレイヤの分離を容易にしている。

 

補足

スクリプト形式

CGIスクリプトやServletでのレスポンス生成を行う方式。 プログラムですべてをコントロールする。

サーバページ形式

PHP,ASP,JSPなどのテキストページを返すことを主体とした形式。 


5.1.       ビューに関するパターン

3種類のパターンが考えられ、2種類の選択がある。

      トランスフォームビュー

 ビューの変換を行う。 通常はXSLTである。 これは、XMLフォーマットまたは容易にそれに変換できるドメインデータを扱っている場合に、有効である。 コントローラは、適切なXSLTスタイルシートを選別し、モデルから収集されたXMLに適用する。

 XSLTXML文書をHTMLやプレーンテキスト、任意のXML文書などへと自由に変換して出力するプログラム。

      テンプレートビュー

 ページの構造にプレゼンテーションを書き込み、ページにマークをつけて動的な内容が必要なところを示すことができる。 (ASP,JSP,PHP

 これは、多くの能力や柔軟性を提供するが、とても複雑なコードになることもある。 JSPの場合、これを軽減するためにカスタムタグライブラリなどの技術がある。

      ツーステップビュー


 ツーステップでビューを生成するパターンである。 図にシングルステップビューおよびツーステップビューのイメージを示す。

 

 


 シングルステージビュー           ツーステップビュー

 

ツーステップビューは、論理的なプレゼンテーションが同じで異なる画面の場合に有効と考えられる。

たとえば、同じ基本的な予約システムを使用する複数の航空会社などの複数のフロントエンドの顧客にサービスが使用されているWebアプリケーションが考えられる。 論理的な画面の限度内で、異なる第2段階を使用することでフロントエンドごとの外見を得られる。 また、Webブラウザと携帯端末に対し、別々の第2段階を用いたりする場合も考えられる。 (但し、現時点では携帯の能力がどうしても劣ることからUIが大きく異なる場合などには不向きではある。)

 

 

5.2.       コントローラに関するパターン

      ページコントローラ

 ページごとのコントローラ。 ビューとコントローラの2つの役割を併せ持つパターンとなる。 シンプルなWebアプリケーションの場合、このタイプとなるケースが多いと考えられる。

      フロントコントローラ

 すべてのページのリクエストを処理するコントローラ。 URLを解釈し、適切なオブジェクトを生成し、処理を委譲する。 このパターンは、Webサイトのアクション構造の変更によるWebサイトの再構成を回避することができる。 Strutsなどもこのタイプであり、構成定義を行うことで構造の変更を行うことができる。

 

 

 

 

 


6.        並行性

 

6.1.       並行性の問題

 複数のプロセスやスレッドが同じデータを処理するときは常に並行性が問題になる。 エンタープライズアプリケーションでは、特にデータベースおよびアプリケーションサーバが問題となる。

 まず、本質的な問題を整理する。 

もっともわかりやすい問題は、更新結果が失われることだろう。 たとえば、英志(ひでし)がファイルを編集してcheckConcurrencyメソッドを変更するとしよう。 このとき、和郎(かずお)も同時に同じファイルのupdateImportantParameterメソッドを編集しようとしているとしよう。 和郎(かずお)英志(ひでし)よりも後に作業を始めてのだが、英志(ひでし)より早く作業が終わったとする。 ここで不幸が生じる。 英志(ひでし)がファイルを開いた時点では、和郎(かずお)の更新は反映されていない。 そのため、英志(ひでし)がファイルの編集を終えたとき和郎(かずお)が更新したファイルに上書きすることになり、和郎(かずお)の更新は無に帰してしまう。

 次に一貫性のない読み込みがある。 読み込んだ2つの情報は間違っていないが、正しいものではないという場合である。 たとえば英志(ひでし)が、lockingmultiphaseの2つのサブパッケージを含む並行性パッケージの中にクラスがいくつあるか調べているとする。 Lockingパッケージを見るとそこには7つのクラスがある。 このとき、尚遠(たかとお)から電話があり、難しい質問をされたとしよう。 英志(ひでし)が電話に答えている間に和郎(かずお)4フェーズロックコード内のバグを修正し、7つのクラスがあったlockingパッケージに2つ、5つのクラスのあったmultiphaseパッケージに3つのクラスを追加する。英志(ひでし)が電話を終えてmultiphaseパッケージを見ると、そこには8つのクラスがある。したがって、英志(ひでし)の計算では7+8の15となる。 これは、正しい数ではない。 和郎(かずお)が修正をくわえるまえのクラスの数は7+5の12で、修正後の数は9+8の17である。どちらもその時点では正しい数である。しかし、15はどの時点でも正しくない。 英志(ひでし)は一貫性のないデータを読み込んだことになる。

 これらの問題は正確さ(または安全性)を脅かし、2人が同時に同じデータで作業しなければ起こらなかったかもしれない不正確な振る舞いを引き起こす。

 正確さのみを重要と考えるならば、同じデータを2人で作業できないようにすれば問題は解決する。 しかし、正確さを確保できても並行して作業を行うことができない。 並行性プログラミングの重要な条件として、正確さに加えて即応性も重要である。 アクティビティをいくつ並行して処理できるかということも大事だ。 また、エラーの重要度、頻度、データの平行性の必要性にもよるが、即応性を重視して正確さを犠牲にすることもある。

 


6.2.       実行コンテキスト

 システム内で処理が行われるとき、常に何らかのコンテキスト内、それも複数のコンテキスト内で処理が行われる。

 外部との相互作用という観点では、2つのコンテキストが存在する。 

リクエストとは、ソフトウェアが動作する世界の外部からの呼び出しのことであり、ソフトウェアはこれに対してレスポンスを返す。 

セッションとは、クライアントとサーバが一定時間内に相互作用することである。 セッションはユーザからは一貫した論理シーケンスに見える一連のリクエストから構成される。  一般にセッションはユーザのログインで始まり、クエリーやビジネストランザクションの実行などさまざまな処理を行う。 セッションの最後にユーザはログアウトする。

エンタープライズアプリケーションでは、クライアントからサーバへと、サーバから別のシステムへという角度から見ることができる。 HTTPセッションと各種データベースなどへのセッションなど、複数のセッションが同時進行する。

OSから見るとプロセスとスレッドがある。 プロセスは重量級の実行コンテキストで、プロセスで扱う内部データを分離する。 スレッドは、軽量級のアクティブエージェントであり、1つのプロセス内で複数のスレッドが動作することができる。 現在、1つのプロセス内で複数のリクエストに対応でき、リソースを効率的に使用できるスレッドの方が好まれている。 しかし、スレッドはメモリを共有するのが一般的で、これが、並行性の問題を引き起こす。 CGIでは、リクエストごとにプロセスを開始していた。 しかし、プロセスを開始するには多くのリソースが必要なため、最近ではあまり使われなくなった。

 データベースの場合は、トランザクションという重要なコンテキストがある。 トランザクションでは、クライアント(またはクライアントプログラム)からの複数のリクエストをあたかも1つのリクエストであるかのように扱う。

 


6.3.       分離性および不変性

 分離性は、データを区切ってそこに1つのアクティブエージェントしかアクセスできないようにする方法である。 OSのメモリ内では、1つのプロセスに対しメモリを排他的に割り当て、そのプロセスがリンクしているデータの読み書きを実行できるようにしている。 このような例としては、Office製品でファイルがロックされるのを経験したことがあるだろう。 たとえば英志(ひでし)があるファイルを開いているときは、そのファイルは他の人は開けられなくなる。 開けた場合でも、英志(ひでし)が作業を始める前のファイルのコピーを読み取り専用で開くだけで変更することはできず、英志(ひでし)が加えている変更もそのファイルでは見ることができないのと同じである。

 分離はエラーを起こしにくくするという意味でとても重要な技法である。 常に並行性に注意していなければならない技法を使用したために、問題が起こっている場合がとても多い。 分離を行えば、プログラムが分離された領域内に挿入される。 この領域内では並行性について懸念する必要はない。 つまり、優れた並行性設計とは、このような領域を作成する方法を模索し、できる限り多くのプログラムをいずれかの領域で行えるようにすることとなる。

 並行性の問題は、共有しているデータが修正できる場合にしか発生しない。 つまり、不変データを認知することが、並行性のコンフリクト(衝突)を回避する方法の1つとなる。 だが、多くのシステムではデータの修正を主な目的としているためすべてのデータを不変なものにすることはなかなかできない。 そこで、不変データ、または不変であると認知したデータを特定することで、そのデータの並行性を考慮しなくても安心して共有できるようになる。 別の方法としては、データを読み込むだけのアプリケーションを分離して、そのアプリケーションにデータソースのコピーを使用させることで、並行性の制御の懸念をなくすることもできる。

 


6.4.       軽い並行性制御および重い並行性制御

 並行性制御には大きく分けて軽い制御と重い制御の2つの形態がある。 英志(ひでし)和郎(かずお)Customerというファイルを同時に編集しようとした場合を想定しよう。 軽ロックでは両者ともにファイルのコピーを作成し、自由に編集することができるので和郎(かずお)が先に編集を終えた場合、彼は自分の作業を問題なくチェックインできる。並行性制御が機能し始めるのは、英志(ひでし)が変更をコミットしようとしたときだ。 この時点で、ソースコード管理システムが、英志(ひでし)が加えた変更と和郎(かずお)の変更とのコンフリクトを検出して、英志(ひでし)によるコミットは拒否され、その状況の処理は英志(ひでし)の判断に任せられる。 一方の重ロックでは、最初にファイルをチェックアウトした人以外は編集が行えなくなる。 つまり、先に英志(ひでし)がファイルをチェックアウトした場合、和郎(かずお)は、英志(ひでし)が修正を終えて変更をコミットするまで、そのファイルでの作業はできないことになる。 

 この2つの手法にはそれぞれ長所と短所がある。 重ロックの場合は、並行性が低下する。 軽ロックでは、ロックが実際に機能するのはコミットする段階だけなので、ストレスを感じることなく作業を進められるだろう。

軽い手法と重い手法のどちらの並行性制御でロックを選択するかは、コンフリクトの頻度と重要性に左右される。 コンフリクトの頻度がとても低い場合や、たとえ発生した場合でもその影響が大きくないのであれば、軽ロックを使用すべきである。 コンフリクトがユーザに与える影響が大きい場合は、重い手法を使用する必要がある。

 

6.5.       一貫性のない読み込みの防止

 英志(ひでし)Orderクラスを呼び出すCustomerクラスを編集し、同時に和郎(かずお)Orderクラスを編集し、インターフェースを変更する。 そして、先に和郎(かずお)がコンパイルしてチェックインし、続いて英志(ひでし)がコンパイルしてチェックインする。 英志(ひでし)が気づかない間にOrderクラスが変更されているので、すでにこの時点で共有コードは壊れている。

 本質的には、これは一貫性のない読み込みの問題であるが、更新を喪失してしまうことが並行性の問題であると考える人が多いため、見落とされる場合が多い。 重ロックは、読み書きのロックという従来からの方法でこの問題に対処する。 データを読み込むには読み込み(または共有)ロックが書き込みには書き込み(または排他的)ロックが必要になる。 同時に複数のユーザが読み込みロックを行うことができるが、誰かが読み込みロックを実行した時点で、他の人は書き込みロックを行えなくなる。 逆に誰かが書き込みロックを行った時点で、他の人はいずれのロックも行えなくなる。 このようにシステムでは重ロックによって一貫性のない読み込みが回避されている。

 軽ロックでは、データのバージョンを示す何らかのマークによってコンフリクトを検出する。ここで言うマークとは、タイムスタンプまたはカウンタなどである。喪失した更新を検出するため、システムは更新されたデータのバージョンマークと共有データのバージョンマークをチェックし、もし、それらが同一だった場合は更新が許可されバージョンマークも更新される。

 

6.6.       デッドロック

 重い技法特有の問題にデッドロックがある。 たとえば英志(ひでし)Customerファイルを、和郎(かずお)Orderファイルをそれぞれ編集し始めたとしよう。 途中で和郎(かずお)は自分の作業を完了するためにCustomerファイルを編集しなくてはならないことに気付くが、英志(ひでし)がロックしているため待たなくてはならない。 その後、英志(ひでし)Orderファイルを編集する必要があることに気付くが、その時点では和郎(かずお)がロックしている。 この状態がデッドロックだ。 一方が終了しない限り両方ともに先に進めなくなった状態をいう。 デッドロックの対処法としてはいくつか考えられる。 1つにはデッドロックを検出するソフトウェアを利用する方法である。 この場合「犠牲」になる人を選択し、その人の変更を破棄して作業を終了させロックを解除し、他の人先に進めるようにするものである。 デッドロックの検出はとても難しく、「犠牲者」にとっては非常なものである。 同様の手法にすべてのロックに制限時間を設けるものがある。 この手法では、制限時間に達した時点でロックが解除されるため、それまでの作業が犠牲になる可能性もある。 タイムアウトはデッドロック検出より簡単に実装できるが、デッドロックが生じていなくても犠牲になることもある。 他の手法としてデッドロックが起こらないようにする手法もある。 作業を開始した時点ですべてのロックを取得するようにして、それ以上のロックは行えないようにしておくという方法がある。 全員に順番でロックを取得させる方法もある。    ただし、この手法では並行性という意味では多少劣ることとなる。

 

データベース(トランザクション)の問題については、他の書籍・資料にもよく書かれているので割愛する。


6.7.       アプリケーションサーバの並行性

 サーバはどのようにして複数のリクエストを同時に処理し、またサーバ上のアプリケーション設計にどの様に影響するのだろうか。 ロックと同期ブロックを伴うマルチスレッドプログラミングは、とても複雑なものとなる。 簡単には見つからない欠陥が突然出てくることがあるため(並行性のバグの大多数は再現不可能なものである。)99%正常に動作しているシステムでも、突発的に不具合が生じる可能性がある。 同期やロックを明示的に処理する必要性を可能な限り避けることに越した事はない。

 最も簡単に処理する方法は、1つのセッションに1つのプロセスの手法である。 この手法では、各セッションは、それぞれ独自のプロセス空間で実行され、完全に分離されている。 つまr、アプリケーション開発者はマルチスレッド処理に関する心配は一切いらない。 ただし、この手法のデメリットは多くのリソースを消費するということである。 より、効率的にするには、それぞれのプロセスが同時に処理するリクエストを1つに限定しながら、別のセッションからの複数のリクエストを順次処理できるように、プロセスをプールさせることである。 この1つのリクエストに1つのプロセスをプールする手法では、より少ないプロセスで所定のセッション数をサポートできる。 分離性の点でもほとんど問題ない。 理由は厄介なマルチスレッドに対処する必要があまりないからである。1つのセッションに1つのプロセスの手法でリクエストを処理する最大の問題点は、リクエストを処理するために使用したリソースをリクエストが終了した時点で開放しなくてはならない点である。現行のApache mod-perlではこの仕組みが使用され、多くの大規模かつ重要なトランザクション処理システムでも使われている。

1つのリクエストに1つのプロセスの手法でも一定の負荷に対処するためには多くのプロセスを必要とする。 1つのプロセスで複数のスレッドを実行するようになれば、スループットをさらに向上させることができる。 1つのリクエストに1つのスレッドの手法では、各リクエストは、1つのプロセス内の1つのスレッドで処理されることになる。 スレッドが使用するサーバリソースはプロセスよりはるかに少ないため、少ないハードウェアリソースでより多くのリクエストを扱うことができ、サーバもより効率的になる。 一方1つのリクエストに1つのスレッドの手法の問題点は、スレッド間に分離がないため、どのデータにも任意のスレッドからアクセスできてしまうという点である。

1つのリクエストに1つのプロセスの手法を1つのリクエストに1つのスレッドの手法と比べれば、効率という点では劣るが拡張性という点では同程度である。 強固性も向上する。 つまり、1つのスレッドが壊れるとプロセス全体がダウンするため、1つのリクエストに1つのプロセスの手法では、壊れたスレッドの影響を制限することができる。 

1つのリクエストに対して1つのスレッドの手法を使用する場合に最も重要なことは、アプリケーション開発者がマルチスレッドを気にしなくてすむ場所に、分離された領域を作成し、そこで処理を行うことである。 一般的な手法としては、スレッドをリクエストの処理を開始する際に、新たなオブジェクトを作成するようにし、オブジェクトが他のスレッドから見える場所(たとえば静的変数内など)におかれないようにするという方法がある。 これによって、他のスレッドから参照できなくなるため、オブジェクトは分離する事になる。

新しいオブジェクトをセッションごとに作成すれば、並行性関連の多くのバグを回避できると同時に拡張性も向上する。 この戦術は多くのケースで利用できるが、開発者が避けなくてはならない領域は他にもある。1つは静的クラスベースの変数、またはグローバル変数だ。 これらを使用するためには動機が必要となるからである。 これは、シングルトンの場合も当てはまる。 ある種のグローバルメモリが必要な場合は、レジストリの使用を勧める。このパターンは、静的変数に見えるものでも、実際にはスレッド特化のストレージを使用するように実装できる。

 


7.        セッションステート

 

 ショッピングカートの中身はセッションステートになっている。 つまり、カート内のデータは特定のセッションだけに関連したものになっている。状態はビジネストランザクション内にあるため、他のセッションとビジネストランザクションとは分離されている。(さらに言うと、各ビジネストランザクションは1つのセッション内でだけ実行され、各セッションは同時に1つのビジネストランザクションしか処理しないということが前提である。)

 セッションステートはビジネストランザクション内にあるので、トランザクションで一般的に考えられているACID(原始性、一貫性、分離性、耐久性)などのプロパティの多くを持っていることになるが、現状ではこの因果関係が必ずしも理解されているとはいえない。

 興味深い関連性として一貫性への影響を挙げることができる。顧客が保険ポリシーを編集しているとき、ポリシーの現行の状態が顧客によっては規定に会わない可能性もある。顧客が変更すると、それがリクエストとしてシステムに送られ、システムは無効値であると応答する。これらの値はセッションステートの一部となるわけだが、有効な値とはならない。セッションステートではこのような状況が生じる場合が多く、処理中は妥当性確認ルールと合致せず、ビジネストランザクションがコミットされた場合にだけ合致するのである。

 セッションステートでもっとも懸念される問題が、分離性の問題である。多くの人がかかわっているので、顧客がポリシーを編集している間はなにが起こってもおかしくない。最もわかりやすい例を挙げると、2人が同時にポリシーを編集している場合である。ここでの問題は、直接変更するということだけではない。たとえば、ポリシーそのものとCustomer(顧客)レコードという2つのレコードがあるとすると、ポリシーには、CustomerレコードのZipCode(郵便番号)に依存するリスクがある、顧客がポリシーの編集を開始して10分後に何らかの操作でCustomerレコードをオープンし、ZipCodeが見える状態になったとする。そこでその10分の間に他の誰かがZipCodeとリスク値を変更したとしたら、一貫性のない読み込みが起こることとなる。

セッションで保持されているすべてのデータが、セッションステートとしてカウントされるわけではない。 セッションは、実際にはリクエストかには必要のないデータも、パフォーマンスを向上させるためにキャッシュして格納していることもある。キャッシュは振る舞いに影響を与えずに削除できるので、適切な振る舞いのためにリクエスト間に格納される必要があるセッションステートは異なるものである。


 

7.1.       セッションステートを格納する方法

 格納する必要があるとわかったセッションステートは、どのように格納すればいいのだろうか。 考えられる選択肢を基本的な3つに分けてみよう。

 クライアントセッションステートでは、データをクライアント上に格納する。これを行う方法はいくつかある。 つまり、WebプレゼンテーションのためにデータをURLにエンコードするクッキーを使用する、Webフォームのいくつかの隠しフィールドにデータを直列化する、クライアント上のオブジェクト内にデータを保持するなどである。

 サーバセッションステートは、リクエストの間にメモリにデータを保持させるというシンプルな方法である。ただし、直列化されたオブジェクトのような耐久性の優れた場所に、セッションステートを格納するメカニズムを保持している。オブジェクトは、アプリケーションサーバのローカルファイルシステムに格納したり、共有データソース内に配置したりすることもできるが、その場所は、キーとしてセッションIDを持つシンプルなデータベーステーブルや、直列化されたオブジェクトが値になるデータベースになる可能性がある。

 データベースセッションステートもサーバサイドストレージだが、ここでは、データをテーブルとフィールドに分割し、より永続性の優れたデータを格納する方法をとりあげて説明する。

 どの選択肢を使うかを決めるにあたっては、考慮すべき点がいくつかある。まず、クライアントとサーバ間で必要なバンド幅について考える。クライアントセッションステートでは、すべてのリクエストによってセッションデータが回線を通じて転送される必要があるが、フィールドが数個しかない場合は問題にならないとしても、データ量が多くなれば転送量も多くなる。 もちろん、プレゼンテー損で見せなければならないデータを転送する場合もあるが、クライアント側で表示する必要がなくても、サーバが使用するデータはすべて転送されなくてはならない。つまり、格納する必要があるセッションステートの量が極端に少なくない限りは、クライアントセッションステートは使うべきではない。その上、セキュリティと整合性にも注意する必要がある。データを暗号化していない限り、悪意のあるユーザがセッションデータを改ざんする可能性もある。

 セッションデータは分離される必要がある。ほとんどの場合、あるセッションで行われていることが別のセッションの実行に影響をあたえてはいけないからである。たとえば、飛行機の予約を入れたとしても、それが、確定されるまでは他のユーザに影響を及ぼすべきではない。 セッションデータであることの意味には、セッション外からは一切見えないということもあるが、これはデータベースセッションステートを使用するときは厄介なものになる。その理由は、セッションデータをデータベース内に存在するレコードデータから分離しなくてはいけないからである。

 サーバでセッションステートを使う場合は、すぐに使用できる形式である必要がある。 サーバセッションステートの場合、背ションステートはサーバが保持していてクライアントセッションステートもサーバ内部にあるため、希望する形式で配置しておく必要があることが多い。一方データベースセッションステートの場合、データベースまでアクセスし、取得する必要がある(さらに何らかの変換が必要になる場合もある)。つまり、それぞれの手法は、それぞれ異なる方式でシステムに応答する必要があるため、データのサイズや複雑さもここでは影響してくることになる。

 一般向け小売システムを運営している場合、各セッションは大量のデータを扱わないが、多くの待機中のユーザを持つことになる。そのため、パフォーマンスを考慮するとデータベースセッションステートが適切ということになる。一方リースシステムでは、膨大な量のデータをリクエストごとにデータベースへ挿入あるいはデータベースから抽出するという面倒なリスクを抱え込まなければならないため、この場合はサーバセッションステートのほうがよいパフォーマンスを実現できる。

 多くのシステムにとって最大の悩みの種といえば、ユーザがセッションをキャンセルし、「なかったことにしてくれ!」といったときの対処法である。これは、特にB2Cアプリケーションでは厄介な問題となる。その理由は、ユーザは「なかったことにしてくれ!」とも言わずにいなくなってしまい、そのまま帰ってこないからだ。このケースで有効なのが、ユーザを容易に忘れることができるクライアントセッションステートである。別の手法では、一定の時間が経過した時点でキャンセルするようにシステムを設定して、キャンセルされたとわかった時点でセッションステートを破棄できるようにしておく必要がある。

 ユーザがキャンセルしたときのことだけでなく、システムが逆にクラッシュしたり、サーバがダウンしたり、ネットワーク接続が切断したりしてしまうような場合だ。データベースセッションステートではこれら3つのケースに適切に対処することができる。 サーバセッションステートは、セッションオブジェクトが不揮発性のメディアに格納されているか、またはそのメディアがどこにあるかによって存続できるかどうかが決まる。クライアントセッションステートはクライアントがクラッシュした場合は存続できないが、その他の障害では存続できるようにしなければならない。

.NET の基礎知識

Common Language Infrastructure (CLI; 共通言語環境仕様)

言語に依存しない,ソフトウェアの実行環境を仕様化したもの。標準化されてヨーロッパ電子計算機工業会の規格である ECMA-335 となった。この仕様を Windows 上で実装したものが CLR である。

  • CLS (共通言語仕様) は,言語に依らない型,命名規約,継承の仕組み,などを仕様として規定したもので,CLI の一部である。

共通言語ランタイム (CLR)

  • IL コード仮想マシン,JIT を含む,実行時エンジン。
  • ガベージコレクションを含むメモリ管理。
  • 安全性への配慮。
  • CTS (共通型システム) は,CLS のサブセットである。これは CLR が CLI の機能制限版なのではなく,CLS よりもゆるい制約に基づいたプログラムを許容するという意味で,機能拡張となっているとも言える。

クラスライブラリ

  • Windows アプリケーション
  • Web アプリケーション
  • Web サービス

Mono

  • Linux 等の UNIX 系 OS,および Windows をターゲットとする。
  • オープンソース。
  • C#,C,C++,Pascal,COBOL のサポートを目指している。

C# とは?

時代的には C→C++→Java→C# という流れであるが,C# は C++ と Java の中間的な機能を持っていると言われるようである。また,プロパティ,インデクサ,イベントやデリゲートなどといった,コンポーネント指向寄りの機能を備えているという点は,おそらく Visual Basic や Delphi の影響と思われる。

タイプセーフで,オブジェクト指向で,ガベージコレクションを持つという点は Java と同様であるが,COM (Active-X) などの既存のコンポーネントとの親和性では明らかに優れている。

Microsoft は,C# を .NET Framework の主力開発言語であると考えているらしい。

C# プログラミング概要

Hello World

伝統的な Hello, World は,C# では次のようになる。

1: // Hello World
2: class Hello
3: {
4:     static void Main()
5:     {
6:         System.Console.WriteLine("Hello, World.");
7:     }
8: }

コメント

「//」から行末まで, または「/*」から「*/」まではコメントである。

Main メソッド

プログラム内のどこかにプログラムの実行を開始から終了まで制御する Main メソッドを定義する必要がある。Main メソッドはクラス (class) または構造体 (struct) 内の静的 (static) なメソッドとして定義する。返値型は void または int, 引数は string[] または引数なしとすることができる。

// こんにちは、世界。
class Hello
{
    static int Main(string[] args)
    {
        System.Console.WriteLine("こんにちは、世界。");
        return 0;
    }
}

C や C++と違い,引数を string[] としたとき,プログラム名は渡されない。

返値型を int としたとき,Main メソッドが返した値はプログラムの終了ステータスとして扱われる。

入出力

入出力は通常 .NET Framework の機能を利用する。上例では,名前空間 System の Console クラスが持つ,WriteLine メソッドを利用することになる。

C# プログラムの構造

C#プログラムは 1 個以上のファイルからなる。各ファイルは 1 個以上の名前空間を含み, 各名前空間はクラス,構造体,インターフェース,列挙型,デリゲートといった型定義や,他の名前空間を含む。以下は,このすべてを含むファイルの例である。

// C#プログラムの骨組み
using System;

namespace MyNamespace1 
{
    class MyClass1 
    {
    }

    struct MyStruct 
    {
    }

    interface IMyInterface 
    {
    }

    delegate int MyDelegate();

    enum MyEnum 
    {
    }
 
    namespace MyNamespace2 
    {
    }

    class MyClass2 
    {
        public static void Main(string[] args) 
        {
        }
    }
}

C#プログラムの開始と終了

  • Main メソッドは, プログラムのエントリポイントであり, プログラムの開始から終了までを制御する。
  • Main メソッドは, クラスまたは構造体内の静的メソッドである。
  • Main メソッドの返値型は void または int である。
  • Main メソッドは引数を持たないか, または string[] 型の引数を持つ。
  • Main メソッドが引数を持つ場合, 引数にはコマンドライン引数が渡される。
  • プログラム名は引数として渡されない。

アセンブリとグローバルアセンブリキャッシュ

  • アセンブリとは .NET Framework アプリケーションを構成する基本的な単位である。
  • アセンブリにはメタデータと呼ばれる内部バージョン番号やデータ,型の情報が含まれている。
  • アセンブリは,必要となった時点でロードされる。使用されないアセンブリはロードされない。
  • アセンブリには複数のモジュールを含ませることができる。
  • アセンブリは .EXE や .DLL として実現される。
  • グローバルアセンブリキャッシュ (GAC) を用いて,アセンブリを他のアプリケーションと共有することができる。
  • GAC へ配置するアセンブリは,厳密名を定義しなければならない。

データ型

データ型の概要

  • int,char 等の組込みデータ型とクラス,構造体等のユーザ定義データ型がある。
  • 値型と参照型がある。

組込みデータ型

組込みの値型
型名 概要 範囲 サイズ
sbyte 符号付整数 -128~127 1
byte 符号なし整数 0~255 1
short 符号付整数 -32768~32767 2
ushort 符号なし整数 0~65535 2
int 符号付整数 -231~231-1 4
uint 符号なし整数 0~232-1 4
long 符号付整数 -263~263-1 8
ulong 符号なし整数 0~264-1 8
bool 論理型 (false または true) false,true 1
char 文字型 (UCS-2) '\u0000'~'\uFFFF' 2
float IEEE 754 単精度浮動小数点数 ±0, ±1.5×10-45~±3.4×1038,±∞,NaN (有効数字7桁) 4
double IEEE 754 倍精度浮動小数点数 ±0, ±5.0×10-324~±1.7×10308,±∞,NaN (有効数字15~16桁) 8
decimal 10進浮動小数 0,±1.0×10-28~±7.9×1028 (有効数字28~29桁) 16

 

組込みの参照型
型名 概要
object あらゆる型の基底クラス
string 文字列型 (UTF-16)。object から直接派生したクラス

ユーザ定義データ型

ユーザ定義の値型
名称 概要
構造体 キーワード struct を使用して定義する
列挙 キーワード enum を使用して定義する

 

ユーザ定義の参照型
型名 概要
クラス キーワード class を用いて定義する
インターフェース キーワード interface を用いて定義する
配列 同じ型のデータの並びを表現する
デリゲート キーワード delegate を使用して定義する

演算子

演算子
演算子のカテゴリ 演算子
算術 +   -   *   /   %
論理 (ブールおよびビットごと) &   ^   !   ~   &&   ||   true   false
文字列の連結 +
インクリメント、デクリメント ++   --
シフト <<   >>
関係 ==   !=   <   >   <=   >=
代入 =   +=   -=   *=   /=   %=   &=   |=   ^=   <<=   >>=
メンバ アクセス .
添字 []
キャスト ()
条件 ?:
デリゲートの連結と削除 +   -
オブジェクトの作成 new
型情報 as   is   sizeof   typeof
オーバーフローの例外制御 checked   unchecked
間接およびアドレス *   ->   []   &

ボックス化 (boxing) とボックス化解除 (unboxing)

ボックス化とボックス化解除と呼ばれる機能は,値型を参照型として扱うための機能である。ボックス化とは,値型を参照型オブジェクトのインスタンスに詰込むことである。こうすることで値型はガーベージコレクションの対象となるヒープへコピーされる。逆にボックス化解除とはボックス化によって参照型オブジェクトに詰込まれた値型を取出すことである。

int i = 123;
object o = (object)i;        // ボックス化
o = 123;                     // ボックス化。特にキャストは必要ない。
i = (int)o;                  // ボックス化解除。キャストが必要。

ボックス化は単純な代入と比べて新しいオブジェクトを生成するなどの複雑な処理が必要であり,時間がかかる。ボックス化解除は,ボックス化ほどではないが,キャスト処理の分だけ単純な代入よりも時間がかかる。

名前空間

名前空間は,複数のクラスライブラリを同時に使用する場合などで,名前の衝突を防ぐための機能である。

名前空間のメンバにアクセスするには,using キーワードを使用して省略可能にするやり方と,明示的に名前空間を指定してアクセスするやり方がある。

using System;
...
Console.WriteLine("Hello, World");
System.Console.WriteLine("Hello, World");

using キーワードを使用して,名前空間の別名を宣言することもできる。

using com = System.Data.Common;
using sql = System.Data.SqlClient;
...
sql.SqlConnection cn = new sql.SqlConnection(...);
com.DbTransaction tx = cn.BeginTransaction();

名前空間を定義するには namespace キーワードを用いる。名前空間はネストすることができ,その場合 2 通りの書き方がある。

namespace N1
{

    // 名前空間 N1 のメンバ (クラスなど) の宣言

    namespace N2
    {
        // 名前空間 N1.N2 のメンバ
    }
}

namespace N1.N2
{
    // 名前空間 N1.N2 のメンバ
}

オブジェクトとクラス (class) と構造体 (struct)

C# はオブジェクト指向言語であり,ウィンドウやコントロールやデータ構造といったオブジェクトを表現するためにクラスや構造体を使うことになる。

  • オブジェクトとは特定のデータ型の実体を言う。データタイプは実行時に生成されるオブジェクトの設計図である。
  • クラスや構造体を利用して新しいデータ型を定義できる。
  • クラスや構造体は C# アプリケーションのコードやデータを含む塊である。C# アプリケーションは常に最低でも 1 個のクラスを含んでいる。
  • 構造体は特に小さなデータを格納するときには理想的な,手軽なクラスと考えられる。
  • C# のクラスは継承をサポートする。すなわち,既存のクラスをベースに,新しい別のクラスを定義できる。

オブジェクト

オブジェクトとは,データ,振舞い,アイデンティティを持ったプログラミング上の構造である。オブジェクトのデータはフィールド,プロパティ,イベントに含まれ,オブジェクトの振舞いはメソッドとインターフェースで定義される。

オブジェクトがアイデンティティを持つとは,2 つのオブジェクトが全く同じデータを含んでいるからといって,必ずしも「同一」のオブジェクトとは見なされないということである。

C# ではクラスと構造体を通してオブジェクトを定義する。クラスと構造体は,その型のすべてのオブジェクトの役割の設計図を構成する。

  • ウィンドウやコントロールを含めて,C# で扱うすべてのものはオブジェクトである。
  • オブジェクトは,そのクラスや構造体によって定義された鋳型から実体化 (生成) される。
  • オブジェクトが含む情報の取得や変更にはプロパティを使用する。
  • オブジェクトの動作を表すためには,メソッドやイベントを使用する。
  • あらゆる C# のオブジェクトは Object を継承する。

クラス (class)

クラスによってデータ型を定義することができる。クラスを定義すると,そのインスタンス (実体) を生成することが可能となる。クラスの実体を生成すると,その参照が返される。オブジェクトへ割当てられた変数には,オブジェクトのデータそのものではなく,このオブジェクトへの参照が格納される。オブジェクトへの参照は,複数の変数へコピーすることができるが,コピーされた参照はすべて元の同一のオブジェクトを表現することになる。このようなわけでクラスは「参照型」であるという。すべての参照型は System.Object から派生する。

クラスはキーワード class を使って定義する。

public class MyClass : MyBaseClass
{
    // メソッド,プロパティ,フィールド,イベントの定義
}

上例で,クラスの名前が「MyClass」であること,アクセスレベルが public であること,基底クラス MyBaseClass から派生していること,を定義している。アクセスレベルが public であるというのは,どこからでもこのクラスを指定することができることを意味している。基底クラスが MyBaseClass から派生しているとは,基底クラス MyBaseClass のすべてのメンバ (メソッド,プロパティ,フィールド,イベント) を派生クラス MyClass も受継いで持っていることを意味する。この機能のことを「継承」という。基底クラスもまた基底クラスを持てるため,一つのクラスには基底クラスがいくつでもあり得る。派生クラスのオブジェクトは基底クラスのオブジェクトの性質をすべて受継いでいるため,派生クラスのオブジェクトは基底クラスのオブジェクトとして使用することができる。このため,派生クラスへの参照は,基底クラスへの参照へ変換できるようになっている。

基底クラスを指定せずにクラスを定義すると,暗黙のうちに基底クラスが System.Object となる。あらゆるクラスは,その基底クラスを遡って行くと最後には System.Object を基底クラスとして持っている。したがって,すべてのクラスオブジェクトは,System.Object 型であるとして扱うことができる。

public class MyPersonClass
{
    // フィールド
    private string name;
    // コンストラクタ
    public MyPersonClass()
    {
        name = "unknown";
    }
    // メソッド
    public void SetName(string newName)
    {
        name = newName;
    }
}

上例は,フィールドとコンストラクタとメソッドをそれぞれ 1 個ずつ持つパブリックなクラスの例である。このクラスのオブジェクトは次のように new キーワードを用いて生成し,使用できる。

MyPersonClass person1 = new MyPersonClass();
person1.SetName("尚遠");

クラスの概要

  • クラスは参照型である。
  • 単一継承のみをサポートしている。(Java と同様。C++ は多重継承をサポートしている。) つまり,直接の基底クラスは 1 個だけに限定される。
  • インターフェースはいくつでも実装できる。
  • クラス定義は,複数のファイルに分割して記述できる。(C# 2.0)
  • 静的なクラスは,静的なメンバのみを持つ。(C# 2.0)

構造体 (struct)

構造体はキーワード struct を使用して定義する。

public struct MyStruct
{
    // フィールド,プロパティ,メソッド,イベントの定義
}

構造体定義の構文は,クラス定義の場合とほとんど同様であるが,クラスにはない制限がある。

  • インスタンスフィールドの宣言に初期化子は書けない。ただし静的フィールドには初期化子が書ける。
  • デフォルトコンストラクタ (引数なしのコンストラクタ) およびデストラクタを宣言できない。

コンパイラが自動的に生成するデフォルトコンストラクタは,構造体が含むすべてのフィールドをデフォルト構築する。構造体はクラスや他の構造体から派生することはできない。

構造体は値型である。構造体のオブジェクトを生成し,変数へ代入すると,構造体の実体が変数へと格納される。変数をコピーすると構造体自身がコピーされ,コピー元オブジェクトとコピー先オブジェクトの一方に対する変更は他方には影響を与えない。構造体はアイデンティティを持たない。構造体への参照を扱う手段はないため,全く同じ値を持つ構造体同士を区別する手段もないからである。C# のすべての値型は,System.ValueType を継承する。System.ValueType は System.Object を継承する。

値型は参照型へいつでも変換 (ボックス化) できる。

構造体の概要

  • 構造体は値型である。(クラスは参照型である。)
  • 構造体をメソッドの引数として渡す場合はその参照ではなく値が渡される。
  • クラスとは異なり,構造体を実体化するのに new 演算子は必要ない。
  • 構造体にはコンストラクタを宣言できるが,必ず引数を持たなければならない。
  • 構造体は他の構造体やクラスからは派生できないし,構造体からクラスを派生することもできない。すべての構造体は System.ValueType を直接継承し,System.ValueType は System.Object を継承している。
  • 構造体はインターフェースを実装できる。
  • インスタンスフィールドの初期化子は書けない。

構造体の使用方法

構造体は「位置」,「長方形」や「色」といった,小さなオブジェクトを表現するのに適している。このような,オブジェクトをクラスを用いて表現することもできるが,しばしば構造体の方が効率がよくなる。たとえば,「位置」クラスの 1000 個のオブジェクトからなる配列を生成する場合,各位置オブジェクトのためにメモリを確保することになる。

public struct Point
{
    public int x;
    public int y;

    public Point(int p1, int p2)
    {
        x = p1;
        y = p2;
    }
}

構造体にはデフォルトコンストラクタ (引数を持たないコンストラクタ) は定義できない。デフォルトコンストラクタはコンパイラが勝手に用意し,常にすべてのメンバをデフォルト値で初期化する。また,インスタンスフィールドを初期化子で初期化することもできない。

構造体を new 演算子で生成すると,適切なコンストラクタを使用して初期化することができる。クラスとは違い,new 演算子を使わずに生成することができるが,この場合は構造体は初期化されず,すべてのメンバを明示的に初期化するまでは参照できないことになる。

構造体はクラスと違って継承を利用できない。構造体は他の構造体やクラスから派生させることはできないし,クラスの基底クラスとすることもできない。しかしながら,構造体は Object を継承しているし,クラスと同様にインターフェースを実装することもできる。

C++ とは違って,struct キーワードを使いクラスを宣言することはできない。C# では,クラスと構造体は本質的に異なるものである。構造体は値型であるがクラスは参照型である。

参照型が必要な場面はともかく,小さなクラスは構造体で置き換えることで効率が良くなることがある。

using System;
public struct Point
{
    public int x, y;

    public Point(int p1, int p2)
    {
        x = p1;
        y = p2;
    }
}

class MainClass
{
    public static void Main()
    {
        Point myPoiont = new Point();
        Point yourPoint = new Point(10, 10);
        Point hisPoiont;

        hisPoint.x = 20;
        hisPoint.y = 20;

        Console.Write("My Point:    ");
        Console.WriteLine("x = {0}, y = {1}", myPoint.x, myPoint.y);
        Console.Write("Your Point:  ");
        Console.WriteLine("x = {0}, y = {1}", yourPoint.x, yourPoint.y);
        Console.Write("His Point:   ");
        Console.WriteLine("x = {0}, y = {1}", hisPoint.x, hisPoint.y);
    }
}
My Point:    x = 0, y = 0
Your Point:  x = 10, y = 10
His point:   x = 20, y = 20

上例で, myPoint はデフォルトコンストラクタによって, yourPoint は宣言したコンストラクタによって, それぞれ初期化し, hisPoint はメンバ毎に初期化している。

クラスおよび構造体のメンバ

クラスおよび構造体のオブジェクトの持つデータや振舞いを定義するメンバとしては,以下のものがある。

  • メソッド
  • コンストラクタ
  • デストラクタ
  • プロパティ
  • フィールドと定数
  • インデクサ
  • 型 (クラスや構造体など)

クラスおよび構造体のメソッド

C# では,クラスと構造体のメソッドはオブジェクトの動きを定義する。メソッドは本質的には一連の文を含むコードブロックである。メソッドは値を計算し,オブジェクトデータに対する操作を実行し,オブジェクトデータに基づく動作を実行するのに利用される。またこれだけにとどまらず,メソッドは C# 言語で表現できるどんな操作も実行することができる。

メソッドは,クラスブロックの中でアクセスレベル,戻り値の型,メソッド名,それにもしあればメソッドパラメータの型と名前を指定することによって宣言される。メソッド引数は,括弧で囲み,カンマで区切られる。空の括弧は,メソッド引数がないことを示す。

class MyClass
{
    void MyMethod1() {}
    void MyMethod2(int param1) {}
    int MyMethod3(int param1, int param2) {}
}

特定のオブジェクトについてメソッドを呼出すのは,フィールドにアクセスする方法と似ている。オブジェクト名の後にドット,メソッド名,それに括弧で引数を書けばよい。

MyClass myObj = new MyClass();

int MyNumber1 = 1;
int MyNumber2 = 2;

// メソッドの呼出し
myObj.MyMethod1();
myObj.MyMethod2(MyNumber1);
myObj.MyMethod3(MyNumber1, MyNumber2);

メソッド引数

メソッドパラメタはメソッドのブロックの中で利用可能である。デフォルトでは,メソッドパラメタはメソッドを呼出す側によって渡されたオブジェクトのコピーである。つまり,パラメタへの変更は,呼出し元のオブジェクトに反映されない。メソッドのコードブロックでは,パラメタだけでなく,同じクラスで定義された他のメソッド,フィールド,プロパティ,イベントに,直接アクセスすることができる。

class MyClass
{
    int myInt;

    void MyMethod1(int param1)
    {
        myInt = param1;
    }

    void MyMethod2(int myInt)
    {
        this.myInt = myInt;
    }
}

クラスのメンバとパラメタを明示的に区別するには,現在のインスタンスを示す this キーワードが使用できる。上例の MyMethod2 がその例である。このようにパラメタ名がメンバ名と同じ場合に利用できる。

メソッドは値を呼出し元に返すことができる。戻り値型 (メソッド名の前に指定する型) が void でない場合,メソッドは return キーワードを使用することで値を返すことができる。キーワード return の後に戻り値型に適合する値を続ければその値が返され,メソッドの実行はそこで終了する。戻り値型が void であっても,値のない return 文をメソッドの実行を終了させるために利用できる。個の場合,return キーワードがなくても,コードブロックの終端に達すると,メソッドの実行は終了する。一方,void 以外の戻り値型のメソッドは,値を返すために必ず return キーワードを使用する必要がある。

class MyClass
{
    int AddTwoNumbers(int param1, int param2)
    {
        return param1 + param2;
    }

    void MyMethod2(int param)
    {
        System.Console.WriteLine(param);
        // 戻り値型が void であるため, return は不要
    }
}

呼出し元は,その型の値が利用できるどこにでもメソッド呼出しを記述できる。

MyClass myObj = new MyClass();

int result = myObj.AddTwoNumbers(1, 2);     // 戻り値を result に格納
myObj.MyMethod(result);                     // 別のメソッドの引数として渡す

myObj.MyMethod(myObj.AddTwoNumbers(1, 2));  // このように使うこともできる

仲介役の変数を用いて複雑な式を読みやすい単純な式に変形することができる。また,メソッドの戻り値を複数回利用する場合には,必要となることもある。

メソッドパラメータで使用するキーワード

ref,out,param の 3 つのキーワードはメソッドパラメータの振舞いを変えるために用意されている。

デフォルトでは,メソッドに渡されるパラメータ値はコピーであり,メソッド本体での変更は呼出し元には影響を与えない。パラメータが値型 (構造体) の場合,メソッド側が変更した構造体の値を呼出し元が利用する手段はない。パラメータが参照型 (クラス) の場合,パラメータはオブジェクトの実体ではなく,オブジェクト実体への参照であるため,メソッドが変更したオブジェクトの実体を,呼出し元から利用することは可能である。しかしながら,別のオブジェクトへの参照をパラメータに代入するなどのパラメータ自体の変更は,値型のときと同様に呼出し元には影響を与えない。

パラメータに ref キーワードを適用することで,パラメータ値をコピーせずに直接渡し,メソッドの実行が終了する際に,呼出し元に戻すように指示することができる。つまり,メソッド側で変更した後のパラメータを,呼出し元が利用できることになる。ref キーワードはメソッドの宣言時と,そのメソッドを呼出すときに使用する。

class SwapSample
{
    static void SwapInts(ref int i, ref int j)
    {
        int t = i;
        i = j;
        j = t;
    }

    public static void Main()
    {
        int a = 123;
        int b = 456;
        System.Console.WriteLine("SwapInts 実行前: {0} {1}", a, b);
        SwapInts(ref a, ref b);                                     // ref を使用して呼出す
        System.Console.WriteLine("SwapInts 実行後: {0} {1}", a, b);
    }
}

out キーワードは,パラメータがデータを呼出し元へ返すためのパラメータであることを示す。呼出し元はパラメータを指定し,メソッドがそのパラメータに新しい値を代入することになる。out パラメータは 1 つ以上の値を返す必要があるメソッドに使用する。ref パラメータとは異なり,out パラメータの初期値をメソッド側で使用することはできない。out キーワードはメソッドの宣言時,そのメソッドの呼出し時に使用する。

class DivideSample
{
    static int DivMod(int dividend, int divisor, out int modulo)
    {
        modulo = dividend % divisor;
        return dividend / divisor;
    }

    public static void Main()
    {
        int quo;
        int mod;
        quo = DivMod(7, 3, out mod);                                // out を使用して呼出す
        System.Console.WriteLine("7 ÷ 3 = {0} ... {1}", quo, mod);
    }
}

param キーワードはパラメータ配列を作成する際に用いる。パラメータ配列は,メソッドの最後の 1 次元配列型のパラメータでなければならない。呼出し元は,パラメータ配列を 2 通りの方法で利用することができる。すなわち,最後のパラメータとして配列型を渡すこともできるし,配列の要素型として扱える一連のパラメータを渡すこともできる。パラメータを渡さないことも可能である。

class Summary
{
    static int Sum(params int[] list)
    {
        int sum = 0;
        for (int k = 0; k < list.Length; ++k)
            sum += list[k];
        return sum;
    }

    public static void Main()
    {
        int sum = Sum(1, 2, 3);                           // 3 個の引数
        System.Console.WriteLine("1 + 2 + 3 = {0}", sum);
        int[] data = {4, 3, 2};
        sum = Sum(data);                                  // 1 個の配列
        System.Console.WriteLine("4 + 3 + 2 = {0}", sum);
        sum = Sum();                                      // 引数なし
        System.Console.WriteLine("          = {0}", sum);
    }
}

メソッドの修飾子

アクセスレベルは,public,private,protected,internal,protected internal のいずれかである。

アクセスレベル修飾子 通用範囲
public 制限なし
private メソッドを宣言したクラス内のみ
protected メソッドを宣言したクラスと,その派生クラス
internal 自アセンブリ内のみ。通常は,同一 EXE (DLL) 内
protected internal protected の範囲 + intenral の範囲

以下にその他の修飾子を挙げる。

修飾子 意味
static インスタンスについてではなく,インスタンスを指定せずに呼出せるメソッドとする。
virtual 派生クラスで実装をオーバライドできるメソッドとする。
override 基底クラスのメソッドをオーバライドする。
sealed virtual なメソッドを,派生クラスでオーバライドできないようにする。
abstract そのクラスではメソッドの実装を行わず,派生クラスで実装する必要があることを示す。
extern アンマネージドなコードで実装されているメソッドを宣言する。
unsafe メソッド内で,ポインタ演算などの安全でないコードをサポートすることを示す。

コンストラクタ

コンストラクタは,クラスまたは構造体名を名前とする特殊なメソッドである。

インスタンスコンストラクタは,new キーワードを用いてインスタンスを生成する際,オブジェクトの実体を生成した直後に呼出される。インスタンスコンストラクタの主な役割は,インスタンスの初期化である。引数を持たないインスタンスコンストラクタをデフォルトコンストラクタという。

また,静的 (static) コンストラクタは,プログラムの実行時,クラスが最初にアクセスされる前に呼出される特殊なメソッドである。静的コンストラクタの呼出しを制御する方法は用意されていない。

デストラクタ

C# はガベージコレクションをサポートしている。ガベージコレクションとは,生成したオブジェクトが必要なくなったときに特に意識してオブジェクトを解放しなくても,自動的に不要なオブジェクトを検出して解放する仕組みのことである。しかし,リソースの獲得と解放について気をつけなければならないケースも存在する。デストラクタはそのような場合に役に立つことがある。

  • デストラクタは構造体にはない。デストラクタが利用できるのはクラスに対してのみである。
  • 1 つのクラスは 1 つまでしかデストラクタを持てない。
  • デストラクタは継承したりオーバロードしたりできない。
  • デストラクタは自動的に呼出される。明示的に呼出すことはできない。
  • デストラクタは修飾子やパラメータを持てない。

プロパティ

プロパティは,クラスや構造体の保持する情報にアクセスする際に利用する。フィールドに直接アクセスするよりも柔軟で安全である。

class Rectangle
{
    private double width;

    public double Width
    {
        get
        {
            return width;
        }
        set
        {
            if (value < 0)
                throw new System.ArgumentException("負の幅を設定しようとした");
            width = value;
        }
    }

    public double Height;

    public double Area
    {
        get
        {
            return width * Height;
        }
    }
}

class MyMain
{
    Rectangle r = new Rectangel();
    r.Width = 6;                         // Width プロパティにアクセス
    r.Height = 7;                        // Height はフィールドを直接アクセス
    System.Console.WriteLine("Area = {0}", r.Area);

    try
    {
        r.Width = -3;                    // Width プロパティに不正な値を入れてみる
    }
    catch (System.ArgumentException)
    {
    }
}

インデクサ

インデクサは,クラスや構造体を配列のようにインデックスで参照するための機能である。インデクサはプロパティと同様の機能であるが,パラメータを持つところが異なっている。

class IntCollection
{
    private int[] myArray = new int[100];

    public int this[int i]
    {
        get
        {
            return myArray[i];
        }
        set
        {
            myArray[i] = value;      // セットする値は「value」で参照できる
        }
    }
}

class MyMain
{
    public static void Main()
    {
        IntCollection c = new IntCollection();
        c[0] = 1;
        c[1] = 2;
        System.Console.WriteLine("{0} {1}", c[0], c[1]);
    }
}

  • オブジェクトを配列のようにインデックスしてアクセスするために用いる。
  • get インデクサは値を返す。set インデクサは値を代入する。
  • インデクサのコードは this キーワードを使用したブロック内に記述する。
  • set インデクサで設定する値は value キーワードで参照する。
  • インデクサは必ずしも整数でインデックスする必要はない。
  • インデクサはオーバロードできる。
  • インデクサは多次元にもできる。たとえば 2 次元配列のようにアクセスできるインデクサも可能である。

class DayCollection
{
    string[] days = {"Sunday", "Monday", "Tuesday", "Wednesday",
                     "Thursday", "Friday", "Saturday"};

    // 曜日の名前を探し,0~6 または -1 を返す。
    private int getday(string testday)
    {
        // 馬鹿サーチ
        int i = 0;
        foreach (string day in days)
        {
            if (day == testday) return i;
            i++;
        }
        return -1;
    }

    // 日曜日~土曜日を 0~6 にして返すインデクサ (読出しのみ)
    public int this[string day]
    {
        get
        {
            return (getday(day));
        }
    }
}

class Program
{
    static void Main(string[] args)
    {
        DayCollection week = new DayCollection();
        Console.WriteLine(week["Friday"]);
        Console.WriteLine(week["Made-up Day"]);
    }
}

配列

配列は同じ型の変数の並びを表現するデータ構造である。配列には 1 次元配列,多次元配列,配列の配列 (jagged 配列) がある。

// 1 次元配列の生成と使用
int[] myIntArray1 = new int[3];
myIntArray1[0] = 1;
myIntArray1[1] = 2;
myIntArray1[2] = 3;

// 1 次元配列を生成し,同時に初期化
int[] myIntArray2 = new int[3]{ 1, 2, 3 };
// 上のコードと全く同じ省略形
int[] myIntArray3 = { 1, 2, 3 };

// 2 次元配列の生成と使用
int[,] myMultidimensionalArray1 = new int[2, 3];
// 同時に初期化
int[,] myMultidimensionalArray2 = { { 1, 2, 3 }, { 4, 5, 6 } };

// jagged 配列
int[][] myJaggedArray = new int[3][];
// jagged 配列の最初の要素 (これは 1 次元配列) を生成
myJaggedArray[0] = new int[4] { 1, 2, 3, 4 };

  • 配列には 1 次元,多次元,jagged の種類がある。
  • 配列の要素は配列を生成した直後は数値型であればすべて 0,参照型であれば null となる。
  • jagged 配列は,配列の配列である。配列は参照型であるため,jagged 配列の生成直後はすべての要素が null である。
  • 配列のインデックスは常に 0 から始まる。したがって要素数が n であれば,インデックスは 0 ~ n-1 の範囲となる。
  • 配列要素の型は,値型でも参照型でも良い。配列型でも良い。
  • 配列は System.Array から派生する参照型である。

継承

クラスは別のクラスを継承して定義することができる。継承はクラス宣言のクラス名の後ろに「:」に続けて継承したいクラス (基底クラス) 名を記述することで表現する。

public class MyBaseClass
{
    int a;

    public MyBaseClass() {}
    public void DoSomething() { ... }
}

public class MyDerivedClass: MyBaseClass
{
    int b;

    public MyDerivedClass() {}
    public void DoAnother() { ... }
}

上例では MyBaseClass から派生する MyDerivedClass を定義している。派生して定義したクラスを元のクラスの派生クラス,元のクラスを派生クラスの基底クラスという。

派生クラスは,通常基底クラスのすべての性質 (データや動作) を受継ぎ,さらに基底クラスにはない独自の性質を持つことができる。派生クラスは基底クラスとしての性質も兼ね備えているため,基底クラスとして振舞うことができる。このことから,派生クラスのオブジェクトは,基底クラス型として参照することができる。逆に,派生クラスのオブジェクトを参照している基底クラス型の参照は,キャストによって元の派生クラス型の参照に変換することができる。キャストされた基底クラス型の参照が実際に派生クラスのオブジェクトを参照していなかった場合は,キャストは失敗し,System.InvalidCastException が発生する。

MyBaseClass x = new MyDerivedClass();
x.DoSomething();

MyDerivedClass y = (MyDerivedClass)x;

x = new MyBaseClass();
y = (MyDerivedClass)x;                  // InvalidCastExceptioin 発生!

基底クラス型の変数に代入して型変換したからといって,派生クラスの実体が変更されるわけではない。一旦派生クラスのインスタンスとして生成された実体は,不要になって破棄されるまで常に派生クラスのインスタンスとしてのアイデンティティを保ち続ける。

ポリモーフィズム

ポリモーフィズムとは,日本語では多態性または多形性とよばれる概念である。広い意味では,一見同一のコードが,さまざまな意味を持つことを言う。たとえば「x = a + b;」というコードは,a と b が数値型なら「加算」を意味しているが,a と b が文字列型であれば「連結」を意味すというのも,広い意味ではポリモーフィズムである。

一番狭い意味では,オブジェクトの共通のインターフェースを通して処理する手続きが,実行時に確定する実際のオブジェクトの性質によって,多様な動作を行うことができることを言う。

 1: // 抽象図形クラス
 2: class abstract Shape
 3: {
 4:     double positionX;  // 位置 X
 5:     double positionY;  // 位置 Y
 6: 
 7:     // この図形の位置を変更する (移動する)
 8:     void MoveTo(double x, double y)
 9:     {
10:         positionX = x;
11:         positionY = y;
12:     }
13: 
14:     // この図形の面積を計算して返す
15:     abstract double CalcArea();
16: }
17: 
18: // 正方形クラス
19: class Square : Shape
20: {
21:     double side;     // 辺の長さ
22: 
23:     Square( ... ) { ... }
24: 
25:     override double CalcArea()
26:     {
27:         return side * side;
28:     }
29: }
30: 
31: // 長方形クラス
32: class Rectangle : Shape
33: {
34:     double sideA;     // 一辺の長さ
35:     double sideB;     // 他の辺の長さ
36: 
37:     Rectangle( ... ) { ... }
38: 
39:     override double CalcArea()
40:     {
41:         return sideA * sideB;
42:     }
43: }
44: 
45: // 複数の図形集合をひとつの図形として扱う図形
46: class MultiShape : Shape
47: {
48:     Shape[] shapes;
49: 
50:     MultiShape( ... ) { ... }
51: 
52:     override double CalcArea()
53:     {
54:         // すべての図形の総面積を求める
55:         double totalArea = 0;
56:         foreach (Shape shape in shapes)
57:             // この単純なコードは,実はさまざまなメソッドを呼び分ける
58:             totalArea += shape.CalcArea();
59:         return totalArea;
60:     }
61: }
62: 
63: class Canvas
64: {
65:     Shape[] shapes;
66: 
67:     void SomeMethod()
68:     {
69:         ...
70:         foreach (Shape shape in shapes)
71:         {
72:             ...
73:             // このコードは常に Shape クラスの MoveTo メソッドを呼出す
74:             shapes.MoveTo(x, y);
75:             ...
76:         }
77:     }
78: }

インターフェース

インターフェースは,オブジェクトの使われ方のみを定義し,オブジェクトが具体的に持つデータや,動作については関知しない。データや動作はインターフェースを実装するクラスや構造体に完全に任される。

C# は単一継承だけをサポートしているが,実装するインターフェースについては制限がないため,複数の基底クラスから継承する代わりにインターフェースを利用することもある。

デリゲート

C# のデリゲートは,C や C++ の関数ポインタと同じようなものである。デリゲートを使用すると,メソッドへの参照を抽象化でき,実際に呼出されるメソッドを動的に選択することができる。

 1: delegate void MyDelegate(string s);
 2:
 3: class YourClass
 4: {
 5:     public string x;
 6:     public void YourMethod(string s)
 7:     {
 8:         System.Console.WriteLine("YourClass {0}: {1}", x, s);
 9:     }
10: }
11:
12: class MyClass
13: {
14:     static void MyMethod(string s)
15:     {
16:         System.Console.WriteLine("MyClass: {0}", s);
17:     }
18: 
19:     public static void Main()
20:     {
21:         YourClass a = new YourClass();
22:         a.x = "A";
23:         MyDelegate d = new MyDelegate(a.YourMethod);
24:         d("Hello");                             // YourClass A: Hello
25:         d = new MyDelegate(MyMethod);
26:         d("Yahoo");                             // MyClass: Yahoo
27:     }
28: }

上例で,YourClass の YourMethod を呼出す際は YourClass のインスタンスが必要である。デリゲートは,メソッドのコードの位置情報だけでなく,インスタンスの情報も格納されていることが分かる。

デリゲートはイベントと組合わせて利用することが多い。

 

イベント

C# のイベントは,コンポーネントが検出した特定の事象を,クライアントに通知するための機能である。たとえば GUI のボタンコンポーネントは,ボタンが押されたときに何をすれば良いのか分からないため,ボタンが押されたことをクライアントに通知し,処理を委譲する必要がある。その場合,C# ではボタンが押されたことを通知するためのデリゲート型を宣言し,クライアントはイベントにそのデリゲートのインスタンスを登録するという方法を取ることを想定している。

delegate void ButtonClickedEventHandler(object sender, EventArgs e);

class Button
{
    event ButtonClickedHandler Clicked;

    void OnClick(EventArgs e)
    {
        if (Clicked != null)
            Clicked(this, e);
    }

    ... いろいろ ....
}

class Client // 画面アプリケーション
{
    Button cancelButton;

    Client()
    {
        cancelButton = new Button();
        // ボタンのイベントに処理メソッドを接続
        cancelButton.Clicked += new ButtonClickedEventHandler(OnCancel);
    }

    void OnCancel(object sender, EventArgs e)
    {
        ... キャンセル処理を実行する ...
    }
}

安全でないコードとポインタ

C# では,unsafe コンテキストを使用して,ポインタを扱うことができる。

  • OS の API や既存の COM などを利用する場合に必要となることがある。
  • C# コンパイラの unsafe を許可するオプションスイッチを指定しないと,unsafe は使用できない。
  • unsafe を利用して作成したアセンブリは「安全でない」と見なされ,インターネットから直接実行することはできなくなる。

nullable 型 (C# 2.0)

ある型に基づく nullable 型の変数は, 元の型のすべての値に加えて「空値」を表現することができる。この機能は特にデータベースを扱う際など, 保持するべき値がないもしくは存在しないことがあるような状況で有用である。

// nullable 型の例
int? x;
...
if (x != null)
    Console.WriteLine(x.Value);
else
    Console.WriteLine("未定義");

  • 未定義状態を持つ変数を生成する際に用いることができる。
  • 「T?」は「System.Nullable<T>」の省略形である。(Tは何らかのデータ型)
  • HasValue プロパティは,変数が値を保持しているとき true,null であるとき false を返す。
  • Value プロパティは,変数が値を保持しているときその値を返す。変数が値を保持していなければ,System.InvalidOperationException を投げる。
  • nullable 型の変数のデフォルト状態は HasValue が false を返す状態である。

nullable 型の使用方法

nullable 型は,元の型のすべての値に加えて「空値」を表現することができる。その宣言には 2 通りの書き方がある。「T」が元の型とすると,

System.Nullable<T> 変数名

または

T? 変数名

のどちらの表現も同じ意味である。

nullable 型の例

値型なら何でも,それを元の型とした nullable 型を作成できる。

int? i = 10;
double? x = 3.14;
bool? flag = null;
char? letter = 'a';
int?[] myArray = new int?[10];

値型でない型は nullable 型にできない。

string? message = "Hello, World!";    // コンパイルエラー。string は参照型
MyClass? myInstance = new MyClass();  // コンパイルエラー。クラスは参照型

as 演算子は参照型にしか使用できないため,nullable 型と共に利用することはできない。

int a = 5;
object b = a;
int? c = b as int;                    // コンパイルエラー。int は値型

nullable 型のメンバ

HasValue
  bool 型の読出し専用プロパティ。null でない値を保持しているとき true。
Value
  元の型の読出し専用プロパティ。null でない値を保持しているときその値,保持していないときは System.InvalidOperationException を投げる。

型変換

int? x = null;
int y = x;         // コンパイルエラー

int y = (int)x;    // 元の型にキャストできる。しかし x == null の場合は例外が発生する
int y = x.Value;   // OK。しかし x == null の場合は例外が発生する

x = null;          // OK。null はいつでもセットできる。
x = 10;            // OK。元の型の値は,nullable 型に暗黙のうちに変換できる

演算

元の型に対する演算は,nullable 型に対しても適用できる。ただし null 値に対する演算は結果も null となる。(bool 型については特別)

int? x = 10;
x++;                             // x は 11 となる
x = x * 10;                      // x は 110 となる
int? y = null;
x = x + y;                       // x は null となる

nullable 型の比較については注意が必要である。nullable 型が null であるときの大小比較は,常に false となる。つまり,nullable 型の大小比較が false であった場合に,必ずしも逆の大小関係が成り立っているとは限らない。

int? x = null;
if (x >= 3)
    Console.WriteLine("x は 3 以上です。");
else
    Console.WriteLine("x は 3 未満です。");     // ウソ

null 値テスト

これまでの例に何度も見てきたとおり,HasValue プロパティを調べる方法と,null と等値比較する方法が使える。

int? x = 10;
if (x.HasValue) Console.WriteLine("x は値を保持しています。");

int? y = null;
if (y != null) Console.WriteLine("y は値を保持しています。");

?? 演算子

nullable 型の既定値を与えたいとき ?? 演算子が利用できる。

int? x = null;
int i = x ?? -1;    // i に x の値を設定する。ただし,x が null なら,-1 を設定する

int? y = 10;
int? z = x ?? y;    // z に x の値を設定する。ただし,x が null なら,y を設定する

bool?

bool? 型に関する & および | 演算は,やや特殊な規則となっている。

x y x&y x|y
true true true true
true false false true
true null null true
false true false true
false false false false
false null false null
null true null true
null false false null
null null null null

& 演算子の場合は false→null→true の順で「強」く,| 演算子の場合は true→null→false の順で「強」い,と考えると容易に理解できる。

今回取り上げなかった重要な話題

  • 文と式と演算子
  • 新デリゲート (C# 2.0)
  • イテレータ (C# 2.0)
  • ジェネリクス (C# 2.0)
  • リフレクション
  • 属性
  • スレッド
  • インタオペラビリティ
  • XML コードコメント
  • アプリケーションドメイン
  • セキュリティ
  • パフォーマンス
  • クラスライブラリ
  • Visual Studio (Windows Forms,Web サービス,Web アプリケーション)