【COM】【Excel】`Workbook.Worksheets`プロパティが返す型は`Worksheets`じゃない

## 要旨
Excel VBAの`Workbook.Worksheets`プロパティのドキュメントを見ると、このように書かれています。

> 指定したブック内のすべてのワークシートを表す Worksheets コレクションを返します。  [^1]

しかし、そうではない事実がコードを実行すると確認できます。

## VBAでの再現

`Worksheets`プロパティが返す型を確認するため、下記のVBAコードを実行してみます。

```VB
Public Sub VBASample()

    Dim app As Excel.Application
    Dim wb As Excel.Workbook
    
    Set app = New Excel.Application
    app.Visible = True
    Set wb = app.Workbooks.Add
    Debug.Print "Type of Worksheets: ", TypeName(wb.Worksheets)
    app.Quit

End Sub
```

すると、`Worksheets`プロパティは実際には`Sheets`型を返していることがわかります。

```
Type of Worksheets:         Sheets
```

`Sheets`オブジェクト[^2]は`Worksheets`オブジェクト[^3]のエイリアスというわけではなく、別のオブジェクトです。

**`Sheets`コレクションは`Worksheet`[^4]と`Chart`[^5] (そしてシートを右クリックしたメニューの「挿入」から追加できるExcel5.0ダイアログシートやExcel4.0マクロシートも含む)をアイテムとして扱えますが、`Worksheets`コレクションは`Worksheet`しかアイテムとして扱えません。**

ワークシートとチャートをひっくるめて扱っている`Sheets`プロパティ[^6]は`Sheets`型を返して型名とプロパティ名が一致しています。
一方でワークシートだけ扱う`Worksheets`プロパティが`Worksheets`型ではなく`Sheets`型なのはなかなかにややこしいです。

```VB
    Debug.Print "Type of Sheets: ", TypeName(wb.Sheets)
```

```
Type of Sheets:             Sheets
```

### 備考: `Workbook.Charts`プロパティの場合

ちなみに`Charts`プロパティがどうかというと、これも`Sheets`型です。

```vb
    Debug.Print "Type of Charts: ", TypeName(wb.Charts)
```

```
Type of Charts:             Sheets
```

しかしこちらについて`Charts`プロパティのドキュメント[^7]には`Sheets`コレクションを返すと書いてある分、まだ親切です。

> 指定したブック内のすべてのグラフ シートを表す Sheets コレクションを返します。


## Pythonと`comtypes`での再現
COM型ライブラリ情報に基づいて、COMインターフェースをPythonのクラスとして定義したモジュールを生成できる`comtypes`[^8]でもこの挙動は再現されます。

```py
>>> from comtypes.client import CreateObject 
>>> app = CreateObject("Excel.Application")
>>> app.Visible = True
>>> wb = app.Workbooks.Add()
>>> print(wb.Worksheets)  # doctest: +ELLIPSIS
<POINTER(Sheets) ptr=... at ...>
>>> app.Quit()
0
>>> exit()
```

## 実行時の振る舞いについて

VBAにしてもPython+`comtypes`にしても、実行時にはたとえ返り値が`Sheets`型であっても`Workbook.Worksheets`プロパティが返すコレクションの中身は`Worksheet`のみであり、`Chart`が紛れているということはありません。

これは下記のVBAコードを実行することで確認できます。

```vb
Sub VBASample2()

    ' Sheet1とChart1があるワークブックで実行
    Dim wb As Excel.Workbook
    Dim sh As Variant
    
    Set wb = ThisWorkbook
    
    For Each sh In wb.Sheets
        Debug.Print "Sheets:", sh.Name, TypeName(sh)
    Next sh
    
    Debug.Print "========"

    For Each sh In wb.Worksheets
        Debug.Print "Worksheets:", sh.Name, TypeName(sh)
    Next sh

    Debug.Print "========"

    For Each sh In wb.Charts
        Debug.Print "Charts:", sh.Name, TypeName(sh)
    Next sh

End Sub

```

```
Sheets:       Sheet1        Worksheet
Sheets:       Chart1        Chart
========
Worksheets:   Sheet1        Worksheet
========
Charts:       Chart1        Chart
```

## この仕様が記載されているドキュメント

`Worksheets`プロパティが`Sheets`型を返す記載が全くないわけではありません。
なんと`Application.Worksheets`のドキュメント[^9]にあります。

> Workbook オブジェクトの場合は、指定したブック内のすべてのワークシートを表す Sheets コレクションを返します。

なぜこんなところに記載されているのかは想像にすぎませんが`Worksheets`プロパティの仕様を書いた他のドキュメントから転用するときに紛れてしまったのかもしれません。

英語ドキュメントの構文部分には

> Syntax
> expression.Worksheets

があり、元になったドキュメントは汎用的に`Worksheets`プロパティを説明していただろうことが推測できます。

## これで困ることは何なのか
`Sheets`型を返すメソッドやプロパティを呼び出す際、ワークシートのコレクションなのかチャートが入るコレクションなのかを区別する必要があります。

特に、ワークシートにはセルの概念がありますが、チャートはセルを持ちません。
なので、不用意にコレクションアイテムのセルにアクセスしようとしたときに、それがチャートだとエラーを起こします。

`Sheets`と`Worksheets`は紛らわしいので、コードレビューをすり抜けてバグを生みかねません。

## どう混同を防止するか
私が参画しているPythonプロジェクトでは、コア機能提供ライブラリ内にワークブックCOMインターフェースの振る舞いをラップするオブジェクトを作り、それに**ワークシートだけがアイテムとなるコレクション**を返すプロパティを実装しています。
これによって、プロジェクトに参画した開発者が`Sheets`プロパティか`Worksheets`プロパティどちらを使えばいいか迷うことなくワークシートを簡単に取得でき、バグが入り込みにくいようにしています。

## 最後に
ExcelのCOMライブラリ内には`Sheets`や`Worksheets`のように混同しやすいものの他にも、メソッドの返り値の型について遅延評価を前提に作られている部分があり、型安全なプログラミングをやりづらい部分があります。
私は`comtypes`をラップしたExcel操作オープンソースライブラリを作ろうとしていますが、それらが原因で仕様を決めかねています。
ランタイムコードを定義するのも、型スタブを定義するのも、コード量が膨大になってしまうアイディアしかまだ浮かばない状況です。
いいアイディアがあれば、この記事のコメントなどで頂けると幸いです。


[^1]: https://learn.microsoft.com/ja-jp/office/vba/api/excel.workbook.worksheets
[^2]: https://learn.microsoft.com/ja-jp/office/vba/api/excel.sheets
[^3]: https://learn.microsoft.com/ja-jp/office/vba/api/excel.worksheets
[^4]: https://learn.microsoft.com/ja-jp/office/vba/api/excel.worksheet
[^5]: https://learn.microsoft.com/ja-jp/office/vba/api/excel.chart(object)
[^6]: https://learn.microsoft.com/ja-jp/office/vba/api/excel.workbook.sheets
[^7]: https://learn.microsoft.com/ja-jp/office/vba/api/excel.workbook.charts
[^8]: https://github.com/enthought/comtypes
[^9]: https://learn.microsoft.com/ja-jp/office/vba/api/excel.application.worksheets