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.RelatedWordList | string[] | 元の単語に関連する複数の単語(同義語など)の配列 |
WdPartOfSpeech.wdVerb | WdPartOfSpeech 列挙型 | 「動詞」を表す品詞情報(Enum) |
つまり、左辺は「単語の配列」、右辺は「品詞を表す列挙値」です。string[] と列挙型 WdPartOfSpeech を == で比較することは論理的に意味がありませんし、C# の型安全性の観点からも許可されません。その結果、
- コンパイル時に型情報がはっきりしていればコンパイルエラー
- dynamic を経由している場合は実行時バインダー例外
という形で表面化します。
Word Interop の SynonymInfo と品詞情報の関係
SynonymInfo クラスの主なプロパティ一覧
品詞判定を正しく行うには、まず SynonymInfo がどんな情報を持っているかを理解しておくとスムーズです。代表的なプロパティを表にまとめると次のようになります。
| プロパティ | 型 | 役割 |
|---|---|---|
Found | bool | 同義語が見つかったかどうか |
MeaningCount | int | 見つかった意味の数(1 から始まるインデックスで利用) |
PartOfSpeechList | object(実際は WdPartOfSpeech の配列) | 各意味に対応する品詞の一覧 |
SynonymList[index] | object(実際は string[]) | 指定した意味に対応する同義語の配列 |
RelatedWordList | object(実際は 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 <= synonymInfo.MeaningCount; i++)
という 1 始まりのループを書くようにしましょう。
コードを一行ずつ分解して理解する
上記の修正版コードを、主要なステップごとに分解してみます。
synonymInfo.Foundの確認if (!synonymInfo.Found) { // 同義語が見つからないときは何もしない or メッセージ表示 return; }同義語データが存在しない場合にムダな処理や例外を防ぐため、最初にFoundをチェックしておくのが安全です。PartOfSpeechListのキャストArray posArray = (Array)synonymInfo.PartOfSpeechList;COM から来る戻り値は C# 側ではobjectなので、まずArrayとして受け取ります。要素自体はWdPartOfSpeechです。- 意味ごとのループ
for (int i = 1; i <= synonymInfo.MeaningCount; i++) { ... }各iは「i 番目の意味」を表します。内側で品詞と同義語をセットで扱うのがポイントです。 - 品詞の取得と判定
var pos = (WdPartOfSpeech)posArray.GetValue(i); if (pos != WdPartOfSpeech.wdVerb) { continue; // 動詞以外はスキップ }ここで初めてWdPartOfSpeech列挙型が登場します。「名詞」「形容詞」など、他の品詞が混ざっていても、wdVerbだけを対象にできます。 - 同義語リストの取得
Array words = (Array)synonymInfo.SynonymList[i];SynonymList[i]は、その意味に対応する候補語だけを返してくれます。ここで初めてstring[](のような配列)を触ります。 - 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<string>(StringComparer.CurrentCultureIgnoreCase);
if (synonymInfo.Found)
{
Array posArray = (Array)synonymInfo.PartOfSpeechList;
for (int i = 1; i <= 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<string>(StringComparer.CurrentCultureIgnoreCase);
for (int i = 1; i <= 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<string> GetVerbSynonyms(
this SynonymInfo synonymInfo)
{
if (synonymInfo == null || !synonymInfo.Found)
yield break;
Array posArray = (Array)synonymInfo.PartOfSpeechList;
for (int i = 1; i <= 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<string> GetSynonymsByPartOfSpeech(
this SynonymInfo synonymInfo,
WdPartOfSpeech targetPos)
{
if (synonymInfo == null || !synonymInfo.Found)
yield break;
Array posArray = (Array)synonymInfo.PartOfSpeechList;
for (int i = 1; i <= 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 の違いを、もう一度整理しておきます。
| プロパティ | 中身 | 用途 |
|---|---|---|
RelatedWordList | string[](単語) | 品詞を意識せず、とりあえず関連語をざっと取得したいとき |
PartOfSpeechList | WdPartOfSpeech[](品詞) | 意味ごとの品詞を判定し、「動詞だけ」「名詞だけ」といったフィルタをかけたいとき |
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 アドイン開発にぜひ活かしてみてください。

コメント