VB.NETのタプル(ValueTuple)でコンパイルエラーになる原因と正しい型指定(名前付きタプル・型推論)

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.ValueTuple0「空タプル」。ほぼ使わない
(A As Integer)System.ValueTuple(Of Integer)11要素
(A As Integer, B As Integer)System.ValueTuple(Of Integer, Integer)22要素
(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を含む非同期処理でもスムーズに扱えます。

この記事を書いた人

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

コメント

コメントする

目次