C# Word アドインで「string[] と WdPartOfSpeech を比較できない」エラーを解決する方法

C# で Word アドイン(VSTO や COM アドイン)を開発していると、「選択された単語が動詞かどうか判定したい」というニーズはよくあります。そのときに SynonymInfo を使ったところ、string[] と WdPartOfSpeech を比較してしまい、実行時バインダー例外に悩まされることがあります。本記事では、エラーの原因から正しい品詞判定の手順、実践的なリファクタリングやパフォーマンス改善のコツまで、Word アドイン開発者向けに詳しく解説します。

目次

エラー「string[] と WdPartOfSpeech を比較できない」の正体

発生しているエラーメッセージ

今回のケースで発生するのは、次のような実行時バインダー例外です。


Operator '==' cannot be applied to operands of type 'string[]' 
and 'Microsoft.Office.Interop.Word.WdPartOfSpeech'

典型的には、以下のようなコードを書いたときに発生します。


using Microsoft.Office.Interop.Word;

...

SynonymInfo synonymInfo = app.SynonymInfo[selectedWord];

if (synonymInfo.Found)
{
    // 誤った比較
    if (synonymInfo.RelatedWordList == WdPartOfSpeech.wdVerb)
    {
        // 動詞のときだけ処理したい……
    }
}

見た目には「左辺も右辺も“単語に関係する何か”」のように見えますが、実際にはまったく別物の型を比較してしまっているため、このようなエラーになります。

比較している型の中身を整理する

問題となっているのは次の 2 つの型です。

要素型意味
synonymInfo.RelatedWordListstring[]元の単語に関連する複数の単語(同義語など)の配列
WdPartOfSpeech.wdVerbWdPartOfSpeech 列挙型「動詞」を表す品詞情報(Enum)

つまり、左辺は「単語の配列」、右辺は「品詞を表す列挙値」です。string[] と列挙型 WdPartOfSpeech を == で比較することは論理的に意味がありませんし、C# の型安全性の観点からも許可されません。その結果、

  • コンパイル時に型情報がはっきりしていればコンパイルエラー
  • dynamic を経由している場合は実行時バインダー例外

という形で表面化します。

Word Interop の SynonymInfo と品詞情報の関係

SynonymInfo クラスの主なプロパティ一覧

品詞判定を正しく行うには、まず SynonymInfo がどんな情報を持っているかを理解しておくとスムーズです。代表的なプロパティを表にまとめると次のようになります。

プロパティ型役割
Foundbool同義語が見つかったかどうか
MeaningCountint見つかった意味の数(1 から始まるインデックスで利用)
PartOfSpeechListobject(実際は WdPartOfSpeech の配列)各意味に対応する品詞の一覧
SynonymList[index]object(実際は string[])指定した意味に対応する同義語の配列
RelatedWordListobject(実際は string[])全体を通して関連する単語の配列(品詞は混在)

今回やりたいのは「動詞かどうか判定」なので、使うべきは RelatedWordList ではなく PartOfSpeechList です。

品詞情報は PartOfSpeechList から取得する

PartOfSpeechList は COM インターフェイスの都合上 object で返ってきますが、中身は WdPartOfSpeech の配列です。そのため、C# 側ではいったん Array にキャストして扱うのが定番です。


Array posArray = (Array)synonymInfo.PartOfSpeechList;

この配列の 1 〜 MeaningCount 番目の要素を順番に見ていき、「その意味の品詞が動詞(WdPartOfSpeech.wdVerb)であれば、その意味に紐づく同義語リストを使う」というのが正しい流れになります。

動詞だけを抽出する正しいコード例

基本的な実装例

まずは、Word の選択範囲から動詞の同義語だけを取り出して ListBox に表示する、最小構成のコード例を示します。


using Microsoft.Office.Interop.Word;

...

SynonymInfo synonymInfo = app.SynonymInfo[selectedWord];

if (synonymInfo.Found)
{
    // 品詞の配列を取得(1 ベース)
    Array posArray = (Array)synonymInfo.PartOfSpeechList;

    for (int i = 1; i <= synonymInfo.MeaningCount; i++)
    {
        // 品詞を取り出して列挙型にキャスト
        var pos = (WdPartOfSpeech)posArray.GetValue(i);

        if (pos == WdPartOfSpeech.wdVerb)
        {
            // i 番目の意味に対応する同義語リスト
            Array words = (Array)synonymInfo.SynonymList[i];

            foreach (string w in words)
            {
                LBSelectedVerbs.Items.Add(w);
            }
        }
    }
}

このコードでは、RelatedWordList は一切使っていません。代わりに、

  • PartOfSpeechList で品詞を判定
  • SynonymList[i] で該当する意味の候補語を取得

という 2 段階で処理を行っています。

1 ベースのインデックスに注意する

Word Interop の SynonymInfo は、配列のインデックスが 1 から始まる点が C# 開発者にとって最大の落とし穴です。普段の C# 配列や List<T> の感覚で for (int i = 0; i < MeaningCount; i++) と書いてしまうと、

  • 0 番目の要素を読もうとして例外になる
  • 最後の意味を 1 件読み落とす

といったバグにつながります。そのため、必ず


for (int i = 1; i &lt;= synonymInfo.MeaningCount; i++)

という 1 始まりのループを書くようにしましょう。

コードを一行ずつ分解して理解する

上記の修正版コードを、主要なステップごとに分解してみます。

  1. synonymInfo.Found の確認 if (!synonymInfo.Found) { // 同義語が見つからないときは何もしない or メッセージ表示 return; } 同義語データが存在しない場合にムダな処理や例外を防ぐため、最初に Found をチェックしておくのが安全です。
  2. PartOfSpeechList のキャスト Array posArray = (Array)synonymInfo.PartOfSpeechList; COM から来る戻り値は C# 側では object なので、まず Array として受け取ります。要素自体は WdPartOfSpeech です。
  3. 意味ごとのループ for (int i = 1; i <= synonymInfo.MeaningCount; i++) { ... } 各 i は「i 番目の意味」を表します。内側で品詞と同義語をセットで扱うのがポイントです。
  4. 品詞の取得と判定 var pos = (WdPartOfSpeech)posArray.GetValue(i); if (pos != WdPartOfSpeech.wdVerb) { continue; // 動詞以外はスキップ } ここで初めて WdPartOfSpeech 列挙型が登場します。「名詞」「形容詞」など、他の品詞が混ざっていても、wdVerb だけを対象にできます。
  5. 同義語リストの取得 Array words = (Array)synonymInfo.SynonymList[i]; SynonymList[i] は、その意味に対応する候補語だけを返してくれます。ここで初めて string[](のような配列)を触ります。
  6. UI への追加 foreach (string w in words) { LBSelectedVerbs.Items.Add(w); } SynonymInfo から取り出した単語をそのまま ListBox に追加しています。後述の通り、ここで重複除去や整形を行うと UI がより使いやすくなります。

より実用的なコードにブラッシュアップする

動詞リストの重複を除去する

Word の辞書上、意味が違っても同じ単語が複数の意味に出てくることがあります。その場合、ListBox に同じ単語が何度も表示されてしまい、ユーザーにとって見づらくなります。

これを防ぐには、HashSet<string> や LINQ の Distinct() を使って重複を取り除きます。ここでは HashSet<string> を使った例を示します。


var verbs = new HashSet&lt;string&gt;(StringComparer.CurrentCultureIgnoreCase);

if (synonymInfo.Found)
{
    Array posArray = (Array)synonymInfo.PartOfSpeechList;

    for (int i = 1; i &lt;= synonymInfo.MeaningCount; i++)
    {
        var pos = (WdPartOfSpeech)posArray.GetValue(i);

        if (pos != WdPartOfSpeech.wdVerb)
            continue;

        Array words = (Array)synonymInfo.SynonymList[i];

        foreach (string w in words)
        {
            if (!string.IsNullOrWhiteSpace(w))
            {
                verbs.Add(w.Trim());
            }
        }
    }
}

// UI へまとめて反映
LBSelectedVerbs.BeginUpdate();
LBSelectedVerbs.Items.Clear();

foreach (var v in verbs)
{
    LBSelectedVerbs.Items.Add(v);
}

LBSelectedVerbs.EndUpdate();

ここでは次の工夫を入れています。

  • StringComparer.CurrentCultureIgnoreCase を指定して大文字・小文字の違いによる重複をまとめる
  • Trim() で前後の空白を削除
  • HashSet<string> に一度集めてから UI に反映する

ListBox.BeginUpdate / EndUpdate で描画を高速化

大量に単語を追加するときに ListBox.Items.Add を 1 件ずつ呼び出すと、そのたびに再描画が走るため、UI がガタついたり処理が重くなったりします。WinForms の ListBox には、描画を一時停止するための BeginUpdate() / EndUpdate() メソッドが用意されています。

メソッド役割
BeginUpdate()再描画を一時的に停止する
EndUpdate()再描画を再開し、途中の変更をまとめて反映する

先ほどのコード例のように、


LBSelectedVerbs.BeginUpdate();
LBSelectedVerbs.Items.Clear();

// Add しまくる

LBSelectedVerbs.EndUpdate();

という形で挟んでおけば、多少単語数が多くてもスムーズに動作します。特にテキスト選択を変えるたびに更新するようなアドインでは、こうした UI 最適化が体感速度に効いてきます。

例外・NULL を意識したガードコード

実運用を考えると、次のようなケースにも備えておくと安心です。

  • 選択されているテキストが空・スペースのみ
  • 選択が複数単語にまたがっている
  • SynonymList[i] が null を返す

こうしたケースをケアしたコード例は以下のようになります。


string selectedWord = app.Selection.Text;

if (string.IsNullOrWhiteSpace(selectedWord))
{
    // 必要ならユーザーにメッセージ表示
    return;
}

SynonymInfo synonymInfo = app.SynonymInfo[selectedWord];

if (!synonymInfo.Found || synonymInfo.MeaningCount == 0)
{
    // 同義語が見つからなかった場合の処理
    return;
}

Array posArray = (Array)synonymInfo.PartOfSpeechList;
var verbs = new HashSet&lt;string&gt;(StringComparer.CurrentCultureIgnoreCase);

for (int i = 1; i &lt;= synonymInfo.MeaningCount; i++)
{
    var pos = (WdPartOfSpeech)posArray.GetValue(i);

    if (pos != WdPartOfSpeech.wdVerb)
        continue;

    var synonymObj = synonymInfo.SynonymList[i];
    if (synonymObj == null)
        continue;

    Array words = (Array)synonymObj;

    foreach (string w in words)
    {
        if (!string.IsNullOrWhiteSpace(w))
        {
            verbs.Add(w.Trim());
        }
    }
}

実際には「複数単語の選択はエラーにする」「最初の単語だけを使う」など、要件に合わせて前処理を追加していくとよいでしょう。

拡張メソッドで再利用しやすくする

動詞の同義語取得をメソッド化する

Word アドイン内であちこち「動詞の同義語を取りたい」処理が出てくるなら、SynonymInfo に対する拡張メソッドとして切り出しておくと、コードの重複を減らせます。


public static class SynonymInfoExtensions
{
    public static IEnumerable&lt;string&gt; GetVerbSynonyms(
        this SynonymInfo synonymInfo)
    {
        if (synonymInfo == null || !synonymInfo.Found)
            yield break;

        Array posArray = (Array)synonymInfo.PartOfSpeechList;

        for (int i = 1; i &lt;= synonymInfo.MeaningCount; i++)
        {
            var pos = (WdPartOfSpeech)posArray.GetValue(i);

            if (pos != WdPartOfSpeech.wdVerb)
                continue;

            var synonymObj = synonymInfo.SynonymList[i];
            if (synonymObj == null)
                continue;

            Array words = (Array)synonymObj;

            foreach (string w in words)
            {
                if (!string.IsNullOrWhiteSpace(w))
                {
                    yield return w.Trim();
                }
            }
        }
    }
}

これを使えば、本体側のコードは次のようにかなりシンプルに書けます。


SynonymInfo synonymInfo = app.SynonymInfo[selectedWord];

var verbs = synonymInfo
    .GetVerbSynonyms()
    .Distinct(StringComparer.CurrentCultureIgnoreCase)
    .ToList();

LBSelectedVerbs.BeginUpdate();
LBSelectedVerbs.Items.Clear();

foreach (var v in verbs)
{
    LBSelectedVerbs.Items.Add(v);
}

LBSelectedVerbs.EndUpdate();

このようにロジックを分離しておくと、ユニットテストもしやすくなり、後から「形容詞も欲しい」「品詞を引数で切り替えたい」といった拡張にも対応しやすくなります。

任意の品詞に対応できる汎用メソッド

もう一歩踏み込んで、任意の品詞を指定できるようにしておくと、さらに汎用性が上がります。


public static IEnumerable&lt;string&gt; GetSynonymsByPartOfSpeech(
    this SynonymInfo synonymInfo,
    WdPartOfSpeech targetPos)
{
    if (synonymInfo == null || !synonymInfo.Found)
        yield break;

    Array posArray = (Array)synonymInfo.PartOfSpeechList;

    for (int i = 1; i &lt;= synonymInfo.MeaningCount; i++)
    {
        var pos = (WdPartOfSpeech)posArray.GetValue(i);

        if (pos != targetPos)
            continue;

        var synonymObj = synonymInfo.SynonymList[i];
        if (synonymObj == null)
            continue;

        Array words = (Array)synonymObj;

        foreach (string w in words)
        {
            if (!string.IsNullOrWhiteSpace(w))
            {
                yield return w.Trim();
            }
        }
    }
}

利用側では、


var verbs = synonymInfo
    .GetSynonymsByPartOfSpeech(WdPartOfSpeech.wdVerb)
    .Distinct(StringComparer.CurrentCultureIgnoreCase);

のように呼び出せます。これで「名詞だけ取得したい」「形容詞だけ取得したい」といったケースにも簡単に対応できます。

RelatedWordList と PartOfSpeechList の役割の違い

今回の元凶になった RelatedWordList と、本来使うべき PartOfSpeechList の違いを、もう一度整理しておきます。

プロパティ中身用途
RelatedWordListstring[](単語)品詞を意識せず、とりあえず関連語をざっと取得したいとき
PartOfSpeechListWdPartOfSpeech[](品詞)意味ごとの品詞を判定し、「動詞だけ」「名詞だけ」といったフィルタをかけたいとき
SynonymList[index]string[](単語)特定の意味・品詞に紐づく候補語を取得したいとき

「品詞で判定したい」のに RelatedWordList を使ってしまうと、どれだけがんばっても WdPartOfSpeech とは比較できず、型不一致のエラーを生むだけです。品詞情報は必ず PartOfSpeechList から取る、というルールを頭に入れておくと混乱を防げます。

よくあるハマりポイントと対処法

dynamic 経由だとコンパイルが通ってしまう問題

Word Interop を dynamic で扱っていると、コンパイル時に型チェックが弱くなり、本来コンパイルエラーになるべきコードも通ってしまいます。その典型例が今回の


if (synonymInfo.RelatedWordList == WdPartOfSpeech.wdVerb)

のような比較です。コンパイラは「実行時に型が決まるから、とりあえず OK」と判断してしまうため、実際に実行するまで問題に気付けません。

対策としては、次のような方針が考えられます。

  • できるだけ dynamic を使わず、明示的に型を指定する
  • object から Array や SynonymInfo などに明示的にキャストする
  • 拡張メソッドやヘルパークラスで型を固めてしまう

特にチーム開発では、dynamic に依存しすぎないコーディング規約を作っておくと、今回のような実行時エラーを未然に防ぎやすくなります。

複数単語の選択・改行を含む選択

ユーザーが複数の単語や改行を含む範囲を選択しているとき、SynonymInfo は期待通りの結果を返さないことがあります。そのため、「単語単位でしか使えない」前提で UI を設計する方が安全です。

  • スペースで分割して最初の単語だけを対象にする
  • 複数単語が選択されている場合はメッセージを出して処理しない
  • ダブルクリック選択(単語選択)を前提に使うようユーザーに説明する

といった方針の中から、要件やターゲットユーザーに合わせて決めておくとよいでしょう。

言語設定と結果の違い

SynonymInfo は Word の言語設定の影響も受けます。たとえば英語の単語に対して日本語辞書を前提にすると、そもそも Found が false になることもあります。多言語対応が必要なアドインでは、

  • 選択範囲の言語を調べて適切な辞書を使う
  • サポートする言語を明示し、それ以外は「サポート外」として扱う

などのポリシーをあらかじめ決めておくと、ユーザー体験が安定します。

実装前に確認したいチェックリスト

最後に、今回のような Word アドインで品詞判定を行うときのチェックポイントをまとめます。

項目確認内容
型の理解RelatedWordList が string[]、PartOfSpeechList が WdPartOfSpeech[] であることを整理しているか
インデックスMeaningCount に対するループを 1 ベースで書いているか
Found チェックsynonymInfo.Found を最初に確認し、見つからない場合の処理を用意しているか
NULL・空文字SynonymList[i] や各単語が null / 空文字のときに落ちないようにしているか
重複除去同じ単語が複数回表示されない工夫(HashSet や Distinct())を入れているか
UI 最適化ListBox.BeginUpdate() / EndUpdate() を使って描画のムダを減らしているか
例外対策複数単語選択・改行・対応外言語などのケースを想定したガードがあるか

まとめ:品詞判定は「正しいプロパティ選び」がカギ

本記事では、Word アドインで動詞かどうかを判定しようとした際に発生する「string[] と WdPartOfSpeech を比較できない」エラーについて、その原因と解決策を詳しく解説しました。

  • RelatedWordList は「単語の配列」であり、品詞と比較してはいけない
  • 品詞情報は PartOfSpeechList から取得し、WdPartOfSpeech.wdVerb などと比較する
  • MeaningCount は 1 ベースなので、ループは for (int i = 1; i <= MeaningCount; i++) と書く
  • 同義語リストは SynonymList[i] から取得し、動詞だけ抽出して UI に表示する
  • 重複除去・UI 最適化・例外対策を加えると、実用的な Word アドインになる

C# で Word アドインを開発する際は、「どのプロパティにどんな型のデータが入っているか」を意識することが非常に重要です。一度正しいパターンを押さえておけば、「名詞だけを抽出する」「形容詞の候補を色分けして表示する」など、さまざまな拡張にも応用できます。今回のコードと考え方をベースに、扱いやすく賢い Word アドイン開発にぜひ活かしてみてください。

この記事を書いた人

実務の現場で詰まりがちなポイントを地図にするITブログ「IT trip」を運営。Windows/Office(Teams・Excel)からSQL、サーバ運用、ガジェットまで、再現性のある手順と“なぜそうなるか”を丁寧に解説します。読んだらすぐ試せること、そして迷った人の次の一歩が見えることを大切にしています。

コメント

コメントする

目次