VB.NETで非同期メソッドが返す「名前付きタプル」を受け取るとき、As ValueTupleと書いた瞬間にコンパイルエラーになることがあります。本記事では、なぜ起きるのかを型の仕組みから解説し、実務で迷わない正しい型指定の書き方までまとめます。
結論:ValueTuple(型引数なし)に代入しようとしているのが原因
今回のエラーの核心はとてもシンプルです。戻ってくる値は「4要素のタプル」なのに、受け皿にしているValueTupleは「要素数0の別型」で、相互変換できないためです。
- 戻り値:
System.ValueTuple(Of Double, Double, Double, Double)(4要素) - 受け皿:
System.ValueTuple(0要素)
そのため、VBでは次の宣言は型が一致せず失敗します。
Dim PointData As ValueTuple = Await GetElevation(NewPoint)
まずは現象を整理:質問のコードは何を返しているのか
質問側の非同期関数は、次のような「名前付きタプル」を返します。
Private Async Function GetElevation(pt As Point) _
As Task(Of (Elevation As Double, Lat As Double, Lng As Double, Resolution As Double))
End Function
この戻り値は、見た目は「(Elevation As Double, …)」ですが、実体はジェネリックのValueTupleです。要素数に応じて型が決まります。
| VBのタプル構文 | 内部的な型(概念) | 要素数 | 備考 |
|---|---|---|---|
() | System.ValueTuple | 0 | 「空タプル」。ほぼ使わない |
(A As Integer) | System.ValueTuple(Of Integer) | 1 | 1要素 |
(A As Integer, B As Integer) | System.ValueTuple(Of Integer, Integer) | 2 | 2要素 |
(A As Double, B As Double, C As Double, D As Double) | System.ValueTuple(Of Double, Double, Double, Double) | 4 | 今回の戻り値に相当 |
そして、呼び出し側で型推論に任せると、VBコンパイラが「返ってきたタプル型」をそのまま推論します。
Dim PointData = Await GetElevation(NewPoint)
Debug.WriteLine(PointData) ' 例: (-3385.2111, 9.04, -87.89, 610.81)
なぜAs ValueTupleがダメなのか:ValueTupleは“総称”ではない
ここで多くの人がハマるポイントが、「ValueTupleという名前が、タプル全体の親クラスっぽく見える」ことです。しかし実際にはそうではありません。
System.ValueTupleとSystem.ValueTuple(Of ...)は別の構造体
System.ValueTuple(型引数なし)と、System.ValueTuple(Of T1, T2, ...)(型引数あり)は、どちらも“構造体(Structure)”ですが、別々の型です。クラスの継承のように「4要素タプルはValueTupleの派生」にはなりません。
System.ValueTuple:0要素タプル用System.ValueTuple(Of T1):1要素タプル用System.ValueTuple(Of T1, T2):2要素タプル用- …(要素数ごとに別型が存在)
つまり、次の代入は「4要素タプル」→「0要素タプル」への変換を要求しているのと同じで、暗黙変換が存在しないためエラーになります。
' 返ってくるのは ValueTuple(Of Double, Double, Double, Double)
' 受け皿は ValueTuple(= ValueTuple of 0 elements)
Dim PointData As ValueTuple = Await GetElevation(NewPoint)
補足:ValueTupleに“共通の親”が欲しい場合はどうする?
「要素数や型が違っても、とにかく“タプルとして”1つの変数に入れたい」という目的がある場合、ValueTuple(型引数なし)を受け皿にするのではなく、次のどちらかを検討します。
| 受け皿 | 使いどころ | 注意点 |
|---|---|---|
Object | とりあえず保持して後で条件分岐する | アクセス時にキャストが必要。型安全が下がる |
System.Runtime.CompilerServices.ITuple | 要素数・要素取得を共通インターフェースで扱う | 値型がボックス化されやすい。要素名は扱いにくい |
ただし今回のケースは「4つの値を受け取って使いたい」なので、素直に4要素タプルの型で受けるのが最適です。
正しい型指定方法:明示するなら“4要素タプル”として書く
明示的に型を書きたい場合は、次のいずれかを選ぶと安全です。実務で読みやすく、後から見返しても意図が伝わります。
方法A:ジェネリックのValueTuple(Of ...)として受ける
Dim PointData As ValueTuple(Of Double, Double, Double, Double) =
Await GetElevation(NewPoint)
この場合、アクセスはItem1〜Item4になります。
Dim elev = PointData.Item1 ' Elevation
Dim lat = PointData.Item2 ' Lat
Dim lng = PointData.Item3 ' Lng
Dim res = PointData.Item4 ' Resolution
名前が消えるのはデメリットですが、型としては最も素直です。「要素名を使わない(Itemで十分)」というチームルールならこれでも問題ありません。
方法B:タプル構文をそのまま書く(おすすめ)
VBでは、返り値と同じ「名前付きタプル型」を宣言側にも書けます。要素名でアクセスできるため、読みやすさが一段上がります。
Dim PointData As (Elevation As Double,
Lat As Double,
Lng As Double,
Resolution As Double) =
Await GetElevation(NewPoint)
Debug.WriteLine(PointData.Elevation)
Debug.WriteLine(PointData.Lat)
Debug.WriteLine(PointData.Lng)
Debug.WriteLine(PointData.Resolution)
特に「地図・測位・座標」系の処理は値の意味が重要です。Item1では混乱しやすいので、要素名が生きるこの書き方が向きます。
方法C:型推論に任せる(最短で安全)
宣言側をシンプルにしたいなら、型推論に任せるのも王道です。
Dim PointData = Await GetElevation(NewPoint)
この1行で、VBコンパイラは戻り値のタプル型を推論し、適切な型の変数を作ってくれます。要素名もそのまま使えるため、実務ではこの形が一番多いはずです。
Dim PointData = ...が通る理由:VBの型推論が“戻り値の正体”を知っている
Dim 変数 = 式の形では、VBは右辺の式の型を推論し、その型で変数を宣言します。今回なら右辺はAwait GetElevation(...)なので、Task(Of ...)の中身(タプル)まで解決され、次のような宣言と同等になります。
' ほぼこれと同じ意味になる
Dim PointData As (Elevation As Double,
Lat As Double,
Lng As Double,
Resolution As Double) =
Await GetElevation(NewPoint)
そのため、As ValueTupleのような「意図しない型への代入」さえ書かなければ、余計なキャストなしで自然に動きます。
| 書き方 | 結果 | 要素名 | おすすめ度 |
|---|---|---|---|
Dim x = Await ... | 戻り値から自動推論 | 使える | 高 |
Dim x As (Elevation As Double, ...)= Await ... | 明示型で受ける | 使える | 高 |
Dim x As ValueTuple(Of Double, ...)= Await ... | 明示型で受ける | Item1〜 | 中 |
Dim x As ValueTuple = Await ... | 型不一致で失敗 | — | 不可 |
関連オプション:Option StrictとOption Inferを押さえる
タプル周りのトラブルは、プロジェクト設定の影響で見え方が変わることがあります。ポイントだけ整理しておくと、原因切り分けが楽になります。
| オプション | 推奨 | 今回への影響 | 補足 |
|---|---|---|---|
Option Strict On | 推奨 | 型不一致を早期に検出できる | 「通るはずのない代入」をはっきりエラーにする |
Option Infer On | 推奨 | Dim x = ...の型推論が有効になる | 型推論で受けたい場合は特に重要 |
なお、今回のAs ValueTupleは「そもそも別型」なので、Option StrictをOffにしても解決しません。正しい型で受ける必要があります。
「名前付きタプル」の“名前”はどこまで信用してよいか
タプルの要素名(Elevation/Lat/Lng/Resolution)は、実務では非常に便利です。ただし、ここには重要な性質があります。
要素名は主にコンパイル時の情報で、受け側の型で決まる
同じValueTuple(Of Double, Double, Double, Double)でも、要素名が使えるかどうかは「変数の型注釈」に強く依存します。受け側の型を名前なしにすると、要素名が使えなくなり、Item1等でアクセスすることになります。
Dim named As (Elevation As Double, Lat As Double, Lng As Double, Resolution As Double) =
Await GetElevation(NewPoint)
' 型を変えて受ける(名前なしにする)
Dim unnamed As (Double, Double, Double, Double) = named
Debug.WriteLine(unnamed.Item1) ' OK
' Debug.WriteLine(unnamed.Elevation) ' 使えない
チーム開発では「要素名を意図せず捨てた」ことで、後のメンテで読みづらくなることがあります。名前を活かしたいなら、受け側も同じ名前付きタプルで宣言するのが堅実です。
実務でのおすすめ:タプルで十分か、専用型を切るべきかの判断
タプルは手軽ですが、乱用すると“意味のあるデータ”が単なる並びに見えてしまいます。今回のように「標高・緯度・経度・解像度」という明確な意味を持つセットは、次の基準で使い分けると失敗しにくいです。
| 状況 | おすすめ | 理由 |
|---|---|---|
| 同一メソッド内で一時的に使う | 名前付きタプル | 書く量が少なく、可読性も高い |
| 複数メソッドを跨いで頻繁に渡す | 専用のStructure/Class | 意味が固定化され、拡張にも強い |
| 公開API(他プロジェクト/他言語)として返す | 専用型推奨 | タプル要素名の扱いが言語間で揺れることがある |
例えば、タプルの代わりに次のような専用型を用意すると、プロパティ名も固定され、型ヒントも強くなります。
Public Structure ElevationResult
Public Property Elevation As Double
Public Property Lat As Double
Public Property Lng As Double
Public Property Resolution As Double
End Structure
戻り値も次のようにできます。
Private Async Function GetElevation(pt As Point) As Task(Of ElevationResult)
End Function
この形にすると、タプル特有の「型指定ミス」や「要素名が落ちる問題」を根本的に避けられます。データ構造が今後増える見込みがあるなら、早めに専用型へ寄せるのが結果的に安いことが多いです。
補足:System.Tuple(旧来のタプル)との違いも押さえておく
検索するとTuple(Of ...)とValueTuple(Of ...)が混在して出てきます。VB.NETで現代的に使うのは基本的にValueTuple側です。違いを簡単にまとめます。
| 種類 | 型 | 特徴 | よくある用途 |
|---|---|---|---|
System.Tuple | 参照型(Class) | ヒープ確保。要素はItem1等。古いAPIで見かける | 既存コード互換 |
System.ValueTuple | 値型(Structure) | 軽量。タプル構文と相性が良い。名前付き要素を扱える | 現在の主流 |
環境メモ:古いターゲットでタプルが使えない場合のチェックポイント
もし「タプル構文自体が使えない」「ValueTupleが見つからない」といった別のエラーが出る場合は、環境依存の可能性があります。代表的なチェックポイントは次の通りです。
- Visual Studio / VB言語バージョンがタプルに対応しているか(VB 15以降が目安)
- ターゲットフレームワークが古い場合、
System.ValueTuple関連の参照(パッケージ/アセンブリ)が不足していないか - プロジェクトで
Option Infer Onになっているか(型推論を使う場合)
今回の「As ValueTupleで代入できない」問題は、環境というより型そのものの不一致なので、まずは受け側の型を正しく直すのが最優先です。
よくある動機別:正しい書き方の選び方
実務では次のような動機でAs ValueTupleを書いてしまうことがあります。目的に合わせて「正しい受け方」を選ぶと、再発を防げます。
| よくある動機 | やりたいこと | 正しい書き方 |
|---|---|---|
| 「タプル型だよ」と明示したい | 読み手に意図を伝えたい | As (Elevation As Double, ...) とタプル構文で明示 |
| 要素型だけ揃っていれば良い | 名前は不要 | As ValueTuple(Of Double, Double, Double, Double) |
| タプルを汎用的に扱いたい | 要素数や型がバラバラ | As ITuple または As Object(ただし用途を限定) |
まとめ:VB.NETのタプルは“見た目より型が厳密”
ValueTuple(型引数なし)は0要素タプルで、4要素タプルとは別型- 明示するなら
ValueTuple(Of Double, Double, Double, Double)か、タプル構文(Elevation As Double, ...)を使う - 名前付きで読みやすくしたいなら、受け側も名前付きタプルで宣言するのが安全
- データが育ちそうなら、専用のStructure/Classへ早めに移すと保守が楽
タプルは便利ですが、VB.NETでは「ValueTupleという単語だけで一括りにしない」ことが最大のポイントです。要素数と型が一致する形で受ければ、Awaitを含む非同期処理でもスムーズに扱えます。

コメント