SHOEISHA iD

※旧SEメンバーシップ会員の方は、同じ登録情報(メールアドレス&パスワード)でログインいただけます

DeveloperZine(デベロッパージン)- エンジニアの意思決定を支える技術情報メディア ProductZine

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

特集記事

Collection 2題:「WPFにバインドできる辞書」と「重複を許す検索set」


"辞書"を作る

1.WPFにバインドできる辞書

 前置きが長くなりました...さて、僕が開発屋さんに頼まれてツールを作るとき、決まって必要になるのが辞書:Dictionaryです。検索キー(key)とそれに紐づいた値(value)との対応表ですね。前置きでお見せした「名前と連絡先の表」は辞書の典型例です。

 ListBox/ListViewにバインドさせたObservableCollection<T>は単純な可変長配列であり、こいつを辞書として機能させるには挿入/検索などを自前で実装しなければなりません。辞書として使えるコレクションはないかとライブラリをうろついて、KeyedCollection<TKey,TItem>を見つけました。

 ただし、KeyedCollection<TKey,TItem>をそのまま使うことはできません。WPF上のListBox/ListViewにバインドできるコレクションはinterface INotifyCollectionChangedを実装、要はコレクションに対する要素の追加/削除/変更に応じて適切なイベントを発行しなければなりません。ListBox/ListViewはそのイベントに反応して画面の更新を行いますから。

 幸いなことにKeyedCollection<TKey,TItem>はイベント発行を埋め込むのにうってつけのprotected virtualメソッドがいくつか定義されているので、これらを再定義することで「WPFにバインドできる辞書:ObservableKeyedCollection<TKey,TItem>」を実装しました。

 やっていることはいたって単純、class KeyedCollection<TKey,TItem>とinterface INotifyCollectionChangedから導出し、要素の追加/変更/削除/クリア時に呼び出されるメソッド:InserItem/RemoveItem/SetItem/ClearItems内でそれぞれに応じたイベントを発行するだけ。

list08 ObservableKeyedCollection.cs
using System.Collections.ObjectModel;
using System.Collections.Generic;
using System.Collections.Specialized;
using STeaL.Collections.Specialized;

namespace STeaL.Collections.ObjectModel {

  public abstract class ObservableKeyedCollection<TKey, TItem> : KeyedCollection<TKey, TItem>, INotifyCollectionChanged {
    public ObservableKeyedCollection() : base() { }
    public ObservableKeyedCollection(IEqualityComparer<TKey> comparer) : base(comparer) { }
    public ObservableKeyedCollection(IEqualityComparer<TKey> comparer, int dictionaryCreationThreshold) : base(comparer, dictionaryCreationThreshold) { }

    protected override void InsertItem(int index, TItem item) {
      base.InsertItem(index, item);
      CollectionChanged.NotifyAdd(this, item, index);
    }

    protected override void SetItem(int index, TItem item) {
      base.SetItem(index, item);
      CollectionChanged.NotifyReplace(this, item, index);
    }

    protected override void RemoveItem(int index) {
      TItem item = this[index];
      base.RemoveItem(index);
      CollectionChanged.NotifyRemove(this, item);
    }

    protected override void ClearItems() {
      base.ClearItems();
      CollectionChanged.NotifyReset(this);
    }

    public event NotifyCollectionChangedEventHandler CollectionChanged;
  }
}

2.要素の重複を許す集合

 と、ここでちょっと問題がありまして...こうやって作ったObservableKeyedCollection<TKey,TItem>およびそのベースとなったKeyedCollection<TKey,TItem>は検索キーが同じ複数の要素を格納できないんです。なのでメール,電話,twitterIDなど複数の連絡先を持った人を"名前-連絡先の表"に載せるとなると連絡先がどれか1つに限られてしまいます。いくつものメール・アドレスや電話番号を持っている人なんて、いまどき珍しくもなんともありませんよねぇ。

 ...というわけで、要素の重複を許す辞書を作ることにしました。挿入/削除に多少の時間はかかるものの、実装の楽な「ソート済み配列」を使うことにします。ソート済み配列ならキーを指定して検索をかけたとき、得られる複数(0コ以上)の検索結果が"配列のココからココまで"つまり配列インデクスのペア(2つ組)で得られます。

二分検索

 ソートされた配列から特定の要素を検索するとなれば、使うアルゴリズムはド定番の二分検索でしょうね。IList<T>に対する拡張メソッドで定義してみましょう。2つの要素を比較する関数オブジェクト:Func<T,T,bool>predに基づいてソートされた要素列:list[first]~list[last-1]の中に、ソート状態を維持したまま新たな要素:T valを挿入するときの挿入位置の最小値/最大値を返す関数:lower_bound/upper_boundを実装します。ついでにこの2関数が返す値をペアにして返すequal_rangeや要素が配列内に存在するか否かを調べるbinary_searchも。

list09 lower_upper_bound.cs
using System;
using System.Collections.Generic;
using STeaL.Functional;

namespace STeaL.Algorithm {
  public static partial class IListExtensions {
#region lower_bound
    public static int lower_bound<T>(this IList<T> list, int first, int last, T val, Func<T,T,bool> pred) {
      int count = last - first;
      while ( 0 < count ) {
        int count2 = count / 2;
        int mid = first + count2;
        if ( pred(list[mid], val) ) {
          first = ++mid;
          count -= count2 + 1;
        } else {
          count = count2;
        }
      }
      return first;
    }
#endregion
#region upper_bound
    public static int upper_bound<T>(this IList<T> list, int first, int last, T val, Func<T, T, bool> pred) {
      int count = last - first;
      while (0 < count) {
        int count2 = count / 2;
        int mid = first + count2;
        if (!pred(val,list[mid])) {
          first = ++mid;
          count -= count2 + 1;
        } else {
          count = count2;
        }
      }
      return first;
    }
#endregion
#region equal_range
    public static Tuple<int,int> equal_range<T>(this IList<T> list, int first, int last, T val, Func<T,T,bool> pred) {
      int count = last - first;
      while ( 0 < count ) {
        int count2 = count / 2;
        int mid = first + count2;
        if ( pred(list[mid], val) ) {
          first = ++mid;
          count -= count2 + 1;
        } else if ( pred(val, list[mid]) ) {
          count = count2;
        } else {
          return Tuple.Create(lower_bound(list,first,mid,val,pred), upper_bound(list,mid+1,first+count,val,pred));
        }
      }
      return Tuple.Create(first, first);
    }
#endregion
#region binary_search
    public static bool binary_search<T>(this IList<T> list, int first, int last, T val, Func<T,T,bool> pred, out int index) {
      index = lower_bound(list, first, last, val, pred);
      return ( index != last && !pred(val, list[index]));
    }
    public static bool binary_search<T>(this IList<T> list, int first, int last, T val, Func<T,T,bool> pred) {
      first = lower_bound(list, first, last, val, pred);
      return ( first != last && !pred(val, list[first]));
    }
#endregion
  }
}

次のページ
最後に

この記事は参考になりましたか?

特集記事連載記事一覧

もっと読む

この記事の著者

επιστημη(エピステーメー)

C++に首まで浸かったプログラマ。Microsoft MVP, Visual C++ (2004.01~2018.06) "だった"りわんくま同盟でたまにセッションスピーカやったり中国茶淹れてにわか茶...

※プロフィールは、執筆時点、または直近の記事の寄稿時点での内容です

この記事は参考になりましたか?

この記事をシェア

CodeZine(コードジン)
https://codezine.jp/article/detail/6925 2012/12/28 10:22

イベント

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

新規会員登録無料のご案内

  • ・全ての過去記事が閲覧できます
  • ・会員限定メルマガを受信できます

メールバックナンバー