VB.NET「BC30909」完全解説:クラスライブラリ統合で発生する Enum 公開エラーの原因と対処(Module の既定 Friend に注意)

クラスライブラリの統合やリポジトリ再編のタイミングで、単体では通っていたビルドが一斉に「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 なら NGPublic 型に差し替え
メソッド引数Public Sub SetInfo(info As DeviceInfo)型が Friend なら NGPublic 型に差し替え
イベント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 なら NGPublic 型のみに限定
拡張メソッドPublic Module Ext : <Extension> Sub M(this As DeviceInfo) ...Module が Friend なら NGPublic Module として公開

名前衝突と解決順序の注意(統合時に増える落とし穴)

  • 同名 Module/型:NativeTypes や Common のような汎用名は衝突しやすく、意図しない側の Friend に名前解決されがちです。一意な名前空間(例:Company.Product.Contracts)に統一しましょう。
  • 暗黙の Imports:プロジェクトの 既定名前空間や Imports により、型解決が異なることがあります。統合後は Imports を最小化し、完全修飾名で一度ビルドしてから整理すると安全です。
  • 同名の列挙子:None や Unknown が重複し、誤った Enum にバインドされる例があります。Enum 名を含むプレフィックス(例:DeviceCapabilityFlags.None)で記述し、ビルド後に Imports を適用します。

移行の実務手順(チェックリスト)

  1. ソリューション全体で 公開 API サーフェスを抽出(Public/Protected の型・メンバーを列挙)。
  2. 抽出結果を元に、シグネチャに含まれる型がすべて Public か確認。
  3. Friend なコンテナ(Module/Class)配下の共有型を洗い出し、名前空間直下の Public 型に移動。
  4. 必要に応じて Contracts/Core ライブラリを新設し、列挙体/構造体/DTO/インターフェイスを移設。
  5. 各ライブラリから Contracts のみを参照させ、依存の向きを 1 方向に整える(ランタイム → Contracts、アプリ → どちらも)。
  6. 統合後は 完全修飾名でビルドし、名前解決のブレを排除。通ったら Imports を段階的に戻す。
  7. Extension メソッドの宣言モジュールを Public Module に統一。
  8. ビルドパイプラインに 静的解析/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 に分けて配置。

段階的リファクタリングの例

  1. 検出:ビルドログから BC30909 を抽出し、ファイル/行番号を一覧化。
  2. 分類:問題の型を「列挙体」「構造体」「DTO」「イベント引数」などに分類。
  3. 収束先の決定:Contracts に置くべきものを決め、ファイルを移動。名前空間は Company.Product.Contracts に統一。
  4. 参照更新:すべての参照箇所を Contracts.<Type> に置換。
  5. コンテナの公開レベル整合:Public 型の親(Module/Class/Namespace)が Public であることを再確認。
  6. 改めてビルド:残存する BC30909 を潰す。完了後、Imports Contracts を追加して記述を簡潔化。
  7. 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 が FriendPublic 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 を根本から撲滅しましょう。

この記事を書いた人

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

コメント

コメントする

目次