クラスライブラリの統合やリポジトリ再編のタイミングで、単体では通っていたビルドが一斉に「BC30909: ‘X’ cannot expose type ‘Y’…」で赤く染まる――。根本原因の多くは、VB の Module が既定で Friend(アセンブリ内限定)になることと、「公開 API は公開型のみを露出できる」というコンパイラ規則の組み合わせにあります。本稿では、この罠の正体と実務的な解決策を徹底解説します。
結論(先に要点)
- 症状:複数ライブラリを統合後、「Public メンバーが内部(Friend)型を外部へ露出している」として
BC30909が大量発生。 - 原因:共通の
Enum/StructureをModuleに定義し、Module の既定アクセスが Friendのままだった。 - 対処:
Moduleをやめて名前空間直下にPublic Enum/Structureを置く、もしくはPublic Moduleと明示。さらに、共通型は Core/Contracts ライブラリへ切り出して参照させる。 - ポイント:Public/Protected なメンバーのシグネチャ(引数/戻り値/プロパティ/イベント/ジェネリック型引数/属性引数)には、必ず Public な型のみを使う。
現象の再現コード(最小例)
次のコードは一見問題なく見えますが、公開 API が内部型を外部へ露出しており BC30909 を引き起こします。
' NativeTypes.vb
Friend Module NativeTypes
<Flags()>
Public Enum DeviceCapabilityFlags As UInteger
None = 0UI
Capture = 1UI
Playback = 2UI
Duplex = Capture Or Playback
End Enum
Public Structure DeviceInfo
Public Id As Integer
Public Name As String
Public Caps As DeviceCapabilityFlags
End Structure
End Module
' Device.vb
Public Class Device
Public Property Capabilities As DeviceCapabilityFlags ' ← BC30909
Public Function GetInfo() As DeviceInfo ' ← BC30909
Return Nothing
End Function
End Class
Device は Public ですが、プロパティ型/戻り値型が Friend Module 内の型(実効的に Friend)なので「Public が内部を露出」になりエラーです。
なぜ統合後に一気に露見するのか
- 単体時は見えにくい:ライブラリを個別に作っていると、API 表面に内部型が含まれていても、設計上「同じアセンブリ内で閉じている」ため、別プロジェクトからの利用で露出問題が顕在化しづらい構成になっている場合があります。
- 統合で解決名が変わる:同名の
Module/型が複数存在していると、統合後の名前解決で Friend なほうに解決され、途端に Public API から内部型を露出する形に変化することがあります。 - 依存の向きが明確になる:アセンブリ境界が整理され、Public API が第三者(別アセンブリ)に露出することが明確化すると、コンパイラによる整合性チェックで一斉に検出されます。
背景知識:VB のアクセス修飾と Module の既定
| 対象 | 記述を省略したときの既定 | 公開可否 | 備考 |
|---|---|---|---|
| Module(トップレベル) | Friend Module | 外部アセンブリには非公開 | 明示しない限り全体が Friend。配下の型・メンバーの実効アクセスは親に制限される。 |
| Class / Structure / Enum(トップレベル) | Friend | 外部アセンブリには非公開 | Public を明示しないと公開 API にならない。 |
| Module 内のメンバー | Public(メソッド等) | ただし親が Friend なら実効 Friend | 親のアクセスレベルが上限。親が Friend なら外部公開されない。 |
重要:型の最終的な公開可否は「最も狭いアクセス(親側を含む)」で決まります。Public Enum と書いても、Friend Module の内側なら事実上 Friend です。
解決策その1:Module をやめ、名前空間直下に Public 型を定義
もっとも安全かつ移植性が高い方法は、共有型を 名前空間直下の Public Enum/Structure/Class として定義し直すことです。
' Contracts\DeviceCapabilityFlags.vb
Namespace Contracts
<Flags()>
Public Enum DeviceCapabilityFlags As UInteger
None = 0UI
Capture = 1UI
Playback = 2UI
Duplex = Capture Or Playback
End Enum
End Namespace
' Contracts\DeviceInfo.vb
Namespace Contracts
Public Structure DeviceInfo
Public Id As Integer
Public Name As String
Public Caps As DeviceCapabilityFlags
End Structure
End Namespace
' Device.vb
Imports Contracts
Public Class Device
Public Property Capabilities As DeviceCapabilityFlags
Public Function GetInfo() As DeviceInfo
Return Nothing
End Function
End Class
この形にすれば、公開 API が確実に Public 型のみを露出し、BC30909 を回避できます。
解決策その2:Public Module として明示
やむを得ず Module を維持する場合は、Public Module と明示します。さらに配下の型も Public を明記してください。
Public Module NativeTypes
<Flags()>
Public Enum DeviceCapabilityFlags As UInteger
None = 0UI
Capture = 1UI
Playback = 2UI
Duplex = Capture Or Playback
End Enum
Public Structure DeviceInfo
Public Id As Integer
Public Name As String
Public Caps As DeviceCapabilityFlags
End Structure
End Module
注意点:Module は静的クラス相当で拡張メソッドの宣言場所にもなります。公開 API の一部として利用するなら、親も子もすべて Public と明示するルールを徹底しましょう。
解決策その3:共通型を Core(Contracts)ライブラリへ分離
ライブラリ間で共有される列挙体/構造体/DTO を Contracts/Core などの独立プロジェクトにまとめます。
Solution
├─ Contracts (Class Library)
│ ├─ Public Enum DeviceCapabilityFlags ...
│ └─ Public Structure DeviceInfo ...
├─ DeviceRuntime (Class Library)
│ └─ Public Class Device ...
└─ App (Console/WinForms/WPF etc.)
└─ 参照: Contracts, DeviceRuntime
この構造では、公開境界(公開 API)の所在が明確になり、参照の循環やアクセス違反が発生しにくくなります。バージョニングも簡単です。
「公開 API は公開型のみ」チェックの観点
| 場所 | 例 | 判定 | 対処 |
|---|---|---|---|
| プロパティ | Public Property Capabilities As DeviceCapabilityFlags | 型が Friend なら NG | 型を Public に、または型の定義位置を見直す |
| メソッド戻り値 | Public Function GetInfo() As DeviceInfo | 型が Friend なら NG | Public 型に差し替え |
| メソッド引数 | Public Sub SetInfo(info As DeviceInfo) | 型が Friend なら NG | Public 型に差し替え |
| イベント | Public Event Updated As EventHandler(Of DeviceInfo) | ジェネリック引数が Friend なら NG | イベント引数型を Public に |
| 配列/コレクション | Public Function List() As DeviceInfo() | 要素型が Friend なら NG | 要素型を Public に |
| Nullable / タプル等 | Nullable(Of DeviceInfo) | 内部型を含めば NG | 含まれる型すべて Public に |
| 属性(Attribute)引数 | <SomeAttr(GetType(DeviceInfo))> | 型が Friend なら NG | Public 型のみに限定 |
| 拡張メソッド | Public Module Ext : <Extension> Sub M(this As DeviceInfo) ... | Module が Friend なら NG | Public Module として公開 |
名前衝突と解決順序の注意(統合時に増える落とし穴)
- 同名 Module/型:
NativeTypesやCommonのような汎用名は衝突しやすく、意図しない側のFriendに名前解決されがちです。一意な名前空間(例:Company.Product.Contracts)に統一しましょう。 - 暗黙の Imports:プロジェクトの 既定名前空間や Imports により、型解決が異なることがあります。統合後は
Importsを最小化し、完全修飾名で一度ビルドしてから整理すると安全です。 - 同名の列挙子:
NoneやUnknownが重複し、誤った Enum にバインドされる例があります。Enum名を含むプレフィックス(例:DeviceCapabilityFlags.None)で記述し、ビルド後に Imports を適用します。
移行の実務手順(チェックリスト)
- ソリューション全体で 公開 API サーフェスを抽出(Public/Protected の型・メンバーを列挙)。
- 抽出結果を元に、シグネチャに含まれる型がすべて
Publicか確認。 - Friend なコンテナ(Module/Class)配下の共有型を洗い出し、名前空間直下の Public 型に移動。
- 必要に応じて Contracts/Core ライブラリを新設し、列挙体/構造体/DTO/インターフェイスを移設。
- 各ライブラリから Contracts のみを参照させ、依存の向きを 1 方向に整える(ランタイム → Contracts、アプリ → どちらも)。
- 統合後は 完全修飾名でビルドし、名前解決のブレを排除。通ったら Imports を段階的に戻す。
- Extension メソッドの宣言モジュールを
Public Moduleに統一。 - ビルドパイプラインに 静的解析/API 差分チェックを組み込み、再発を防止。
コード・パターン:安全な公開の書き方
' 好ましい:Contracts 直下の Public 型だけを公開に使う
Namespace Contracts
<Flags()>
Public Enum DeviceCapabilityFlags As UInteger
None = 0UI : Capture = 1UI : Playback = 2UI : Duplex = Capture Or Playback
End Enum
Public Structure DeviceInfo
Public Id As Integer
Public Name As String
Public Caps As DeviceCapabilityFlags
End Structure
Public Interface IDevice
ReadOnly Property Capabilities As DeviceCapabilityFlags
Function GetInfo() As DeviceInfo
End Interface
End Namespace
Public Class Device
Implements Contracts.IDevice
Public ReadOnly Property Capabilities As Contracts.DeviceCapabilityFlags Implements Contracts.IDevice.Capabilities
Get
Return Contracts.DeviceCapabilityFlags.Duplex
End Get
End Property
Public Function GetInfo() As Contracts.DeviceInfo Implements Contracts.IDevice.GetInfo
Return New Contracts.DeviceInfo With {
.Id = 1, .Name = "Mic", .Caps = Contracts.DeviceCapabilityFlags.Capture
}
End Function
End Class
BC30909 の読み方とデバッグのコツ
- 典型メッセージ:
BC30909: 'Capabilities' cannot expose type 'NativeTypes.DeviceCapabilityFlags' outside the project through class 'Device'. - 読み替え:「Device の Public メンバー Capabilities が NativeTypes.DeviceCapabilityFlags(公開ではない型)を外部に晒している」。
- 直す場所:
DeviceCapabilityFlagsの 定義側(コンテナのアクセス)と、利用側(公開 API のシグネチャ)を同時に確認。 - IDE 補助:該当箇所で F12(定義へ移動) → 宣言の先頭にマウスを置き、IDE が示すアクセス修飾(Public/Friend)を再確認。モジュール/クラスの宣言も必ずチェック。
ありがちな誤解・落とし穴
- 「Enum を Public と書いた」=外部公開される:×。親(Module/Class)が Friend なら、子がどれだけ Public でも実効は Friend のまま。
- 拡張メソッドは Public だから大丈夫:×。
Public Moduleで宣言していなければ、その拡張メソッド自体が外部公開されません。 - テスト用に
InternalsVisibleToを付ければよい:△。単体テストには有用ですが、公開 API の露出設計の誤りは隠せません。根本は型の配置とアクセス修飾で解決すべきです。 - 一時回避として API を Friend に落とす:△。短期のビルド通過には使えますが、外部コントラクトが変わるため互換性に影響。推奨は 型の公開レベルを正しく引き上げること。
設計原則:Contracts First(契約優先)
公開 API を先に固定し(インターフェイス/DTO/列挙体)、それらを Contracts として独立パッケージにまとめます。実装(Runtime)は Contracts を参照しますが、その逆はありません。この逆依存を断つだけで、BC30909 の大半は未然に防げます。
- Contracts に置くもの:Public Enum、Public Structure(不変 DTO 推奨)、Public Interface、エラーコード、例外の基底型。
- 実装側にのみ置くもの:内部ヘルパー、キャッシュ、リポジトリ具象、プラットフォーム依存のラッパー、
Friendな実装詳細。
移行を助ける実用テクニック
- 一時的に
Option Strict On:暗黙の変換を抑制し、シグネチャの型を明確化。 - 完全修飾名での一斉置換:
DeviceCapabilityFlags→Contracts.DeviceCapabilityFlagsのように、まず完全修飾へ統一してからImportsを整理。 - 命名規約:列挙体は
XXXFlags、DTO はXXXInfo/XXXDto、名前空間は...Contracts。目で見て「公開/内部」を判別しやすく。 - 拡張メソッドの置き場統一:
Public Module XxxExtensionsをContracts直下に置かない(実装詳細を侵食しがち)。Runtime側で公開が必要なら Public に、内部なら Friend に分けて配置。
段階的リファクタリングの例
- 検出:ビルドログから
BC30909を抽出し、ファイル/行番号を一覧化。 - 分類:問題の型を「列挙体」「構造体」「DTO」「イベント引数」などに分類。
- 収束先の決定:Contracts に置くべきものを決め、ファイルを移動。名前空間は
Company.Product.Contractsに統一。 - 参照更新:すべての参照箇所を
Contracts.<Type>に置換。 - コンテナの公開レベル整合:Public 型の親(Module/Class/Namespace)が Public であることを再確認。
- 改めてビルド:残存する
BC30909を潰す。完了後、Imports Contractsを追加して記述を簡潔化。 - API スナップショット作成:公開 API の一覧をファイル化(公開審査の基準に)。
実案件でのチェック観点(監査テンプレート)
| チェック項目 | 目的 | 判定方法 |
|---|---|---|
| Contracts プロジェクトの有無 | 公開境界の明確化 | ソリューション構成を確認 |
| Public API サーフェスの抽出 | 露出型の棚卸し | Public/Protected の型・メンバー一覧を生成 |
| Friend コンテナ配下の型の公開意図 | 暗黙 Friend の排除 | 親のアクセス修飾を見る(Module/Class が Friend なら要修正) |
| 拡張メソッドの宣言モジュール | 外部公開の可否の整合 | Public Module XxxExtensions になっているか確認 |
| イベント引数/デリゲートの型 | ジェネリック経由の内部露出排除 | EventHandler(Of T) の T が Public か確認 |
FAQ
Q. Public Enum を Friend Module 内に置いてもいい?
A. 非推奨です。親のアクセス(Friend)が上限となり、実効的に外部へ公開されません。将来の再編成でも問題の温床になります。
Q. どうして Public プロパティが内部型を使うとダメなの?
A. コンパイル言語仕様として、より広い可視性(Public)のメンバーが、より狭い可視性(Friend)の型をシグネチャで露出することは許されません。呼び出し側がその型の定義を知り得ないからです。
Q. 既存 API を壊さずに直すには?
A. まず同名の Public 型を Contracts に新設し、既存の内部型を段階的に置換。旧型は Obsolete 属性で段階的廃止するのが安全です。
Q. ランタイム実装の内部最適化に Friend は使ってよい?
A. もちろん有効です。ただし 公開境界の外側(Public API)へ波及しないよう、内部型は公開シグネチャに含めない設計を徹底してください。
まとめ
BC30909 は「公開 API の衛生チェック」が正しく働いているサインです。多くのケースで原因は、共有の列挙体/構造体/DTO を Friend 既定の Module に置いてしまった設計と、統合時の名前解決のズレにあります。Module の利用を最小化し、Public 型を名前空間直下または Contracts ライブラリに集約する――これだけで設計の健全性が大きく高まり、統合や将来の再構成にも強くなります。今日見つかったエラーを、明日の品質基準へと昇華させましょう。
付録:誤りパターンと修正パターン早見表
| 誤りパターン | 修正パターン | 備考 |
|---|---|---|
Friend Module NativeTypes 内に Public Enum | 名前空間直下に Public Enum を移動、または Public Module に格上げ | 親アクセスが上限になる点に注意 |
| Public プロパティが内部構造体を返す | 構造体を Contracts に Public で再定義 | DTO は不変化(イミュータブル)推奨 |
イベント:EventHandler(Of InternalArgs) | Public Class InternalArgs を Contracts に移動 | 引数型は必ず Public |
| 拡張メソッドの Module が Friend | Public Module XxxExtensions にする | API として公開するなら親も Public に |
| 配列/コレクションで内部要素型を露出 | 要素型そのものを Public に | 列挙体・構造体・クラスすべて対象 |
付録:移行時に役立つスクリプト片(PowerShell 例)
Public メンバーのシグネチャに Friend 型が含まれていないか、ざっくり検査するラフな例です(厳密解析ではありません)。
# ざっくり検査:Public メンバーと Friend 型の同居をテキストベースで発見
Get-ChildItem -Recurse -Filter *.vb |
Select-String -Pattern 'Public (Class|Structure|Enum|Interface|Property|Function|Sub|Event)' -List |
ForEach-Object {
$file = $_.Path
$text = Get-Content $file -Raw
if ($text -match 'Friend\s+(Module|Class|Structure|Enum)') {
Write-Host "要確認: $file"
}
}
最終的には IDE のナビゲーションとビルドエラーの行番号で、根拠をもって 1 件ずつ是正するのが確実です。
実装の質をさらに高めるために
- 命名で意味を表す:
DeviceCapabilityFlagsのように用途を明確化。 - バイナリ互換を意識:
Enumの基になる型は安定したUInteger等を明示。 - 非推奨の段階導入:旧 API には
<Obsolete("use Contracts.DeviceInfo")>を付け、利用者に道筋を示す。
まとめの一文
「公開 API = Public 型だけ」――この単純な原則を守るために、Module の既定 Friend と名前解決の落とし穴を正しく理解し、Contracts への集約とアクセス修飾の明示で、BC30909 を根本から撲滅しましょう。

コメント