はじめに
VisualBasic 2017の新機能に「ByRef return」があるのですが、MSDNのVisual Basic Teamが出している情報くらいしか見つけれられません。
そのvbteamブログの記事「What’s New in Visual Basic 2017」と「[Why VB2017 only supports consuming ref returning methods](Why VB2017 only supports consuming ref returning methods)」の一部を日本語化したInfoQの記事「Visual Basic 15の新たな言語機能」となります。
VisualBasic 2017については技術系メディアでも記事にしないので、個人ブログで書く程度でしかないですね、拙い知識ですが自分なりに調べたことを書いてみます。
ByRef return
C#7から、戻り値とローカル変数でも参照渡しを使えるようになりました。
参照:参照戻り値と参照ローカル変数
この参照戻り値を使用したメソッドをVB2017で呼出して使用出来るようにしたのが「ByRef return」となります。
「ByRef return」のサンプルコード「What’s New in Visual Basic 2017」から抜粋
Dim didFind As Boolean
'Version #2: With a simple generic helper-class:
aSentence = New NewInCS2017.Sentence("Adrian is going to marry Adriana, because Adrian loves Adriana.")
Do
VbByRefHelper(aSentence.FindNext("Adr", didFind),
Function(stringFound) As String
If stringFound = "Adrian" Then
stringFound = "Klaus"
Return stringFound
End If
Return stringFound
End Function)
Loop While didfind
「NewInCS2017.Sentence」はC#で書かれています。
「KlausLoeffelmann/vbteamblog-2017 - github」
aSentence = New NewInCS2017.Sentence("Adrian is going to marry Adriana, because Adrian loves Adriana.")
現在のところは、VB2017のみで参照戻り値を使用したりすることは出来ません。
下記のようにByRefキーワードを各箇所で記述してもコンパイルエラーになるだけです。
ByRef Function FindNext(startWithString As String) As String
Dim ByRef myRef = objectExpression
Default Public ReadOnly Property Item(index As Integer) As ByRef Integer
参照戻り値
データ型には「値型(Value type)」と「参照型(Reference Type)」があります。
例えばIntger型は値型でString型は参照型、構造体(struct)は値型でクラス(class)は参照型になります。
※String型は参照型ではあるのですが、値型のような動作をします。参照:(.Net)Stringの仕様にやられた
メソッドの引数の渡し方には下記4パターンがあります。ref/ByRefキーワードを付けることで参照渡しになります。ちなみに最近Javaを使用しているのですが、Javaは値渡しのみです。参照:値渡しと参照渡し (と参照の値渡し)
- 値型の値渡し
- 値型の参照渡し(ref/ByRefを付ける)
- 参照型の値渡し
- 参照型の参照渡し(ref/ByRefを付ける)
戻り値はC#6までは値渡しのみでしたが、C#7でrefキーワードを付けて参照渡しが可能となりました。
参照渡しは変数のメモリ番地を渡す渡し方ですので、ここに null など不定な値が渡されると危険です。ですので値渡しにして安全性を確保しているわけですね。
ちなみに、.NETの仮想マシンレベルでは、最初から戻り値や引数での参照の仕組みを持っている。
(中略)
参照を認めていなかったのはあくまでC#のレベルでの制限である。C#に対してパフォーマンスを向上させる要求が高まったのと、コンパイラーをC#で作り直したことで安全性を保証するためのフロー解析がしやすくなったため、制限が緩和されることになった。
引用:次期C#とパフォーマンス向上(後編)―― 予定・検討中の5つの新機構
パフォーマンス向上
下記コードでは、引数に渡したり戻り値として値を返す場合はメモリ領域が再確保され、そこにコピーが生成されます。そのため、実は値が 3 回コピーされています。
参照:ref 戻り値 / ref ローカル変数
static void Main()
{
var a = 123; //--- 1. メモリ領域確保
var d = PassThrough(a); //--- 4. 戻されたのを受けとるときにもう一回コピー
}
static int PassThrough(int b) //--- 2. 引数として渡すのに値をコピー
{
int c = b; //--- 3. 別の変数として受ける場合もコピー
return c;
}
int 型のような軽量なものなら気になりませんが、サイズの大きな値型となるとコピーによる影響が無視できなくなります。
究極的にパフォーマンスを高く持つためには、値のコピー (とメモリ領域の解放も?) の頻度/回数を減らす必要があります。
下記コードは、参照戻り値を使って配列の要素を一部のみを書き換えています。
static void Main()
{
var a = new int[] { 0, 1, 2, 3, 4 };
Console.WriteLine(string.Join(",", a));
// 0, 1, 2, 3, 4
ref var d = ref GetValue(a); //--- 参照返しを変数 d で受ける
d = 5; //--- 書き換え
Console.WriteLine(string.Join(",", a));
// 0, 1, 5, 3, 4
}
static ref int GetValue(int[] b)
{
ref var c = ref b[2]; //--- b の 3 番目の要素を参照する変数 c を作る
return ref c; //--- 変数 c の参照先を返す
}
参照戻り値メソッド利用に関する機能
現状C#と同等機能を採用できない理由
InfoQの記事「Visual Basic 15の新たな言語機能」のコラム内に記述されています。
大元の記事は「Why VB2017 only supports consuming ref returning methods」です。
要約すると、参照方法についてさまざまな検討と決定が行なわれ、これらすべてをVB内で解決した上で、同等の生産性を実現しなくてはならないが、それには多くの作業が必要。また、VBには「プロパティのByRef渡し」のようなByRefに関わる特別な機能があるため、さらに多くの処理が必要になります。
VBコンパイラの開発責任者であるJared Parsons氏によるVBのByRefに関するさまざまなケースを詳しく解説した素晴らしい記事「The many cases of ByRef」がある。
配列で実行可能なSliceに関する操作
「Why VB2017 only supports consuming ref returning methods」のSlice部分の説明です。
Module Module1
Structure Point
Public X As Integer
Public Y As Integer
End Structure
Structure Slice(Of T)
Private ReadOnly _Array As T()
Private ReadOnly _Offset As Integer
Sub New(array As T(), offset As Integer)
_Array = array
_Offset = offset
End Sub
Default Property Item(index As Integer) As T
Get
Return _Array(_Offset + index)
End Get
Set(value As T)
_Array(_Offset + index) = value
End Set
End Property
End Structure
Sub Main()
Dim pt() As Point = {
New Point With {.X = 10, .Y = 0},
New Point With {.X = 20, .Y = 1},
New Point With {.X = 30, .Y = 2},
New Point With {.X = 40, .Y = 3},
New Point With {.X = 50, .Y = 4},
New Point With {.X = 60, .Y = 5}
}
Dim mySlice As New Slice(Of Point)(pt, 1) ' Offset 1した参照値をセット
' 下記方法はコンパイルエラーとなる。
' Point型をStruct(値型)からClass(参照型)にするとコンパイルエラーにはならない。
'mySlice(3).X += 1
Dim temp = mySlice(3) ' 配列の4番目であるが、Offset 1してるので5番目がセット
temp.X += 1 ' 5番目の値に1加算
mySlice(3) = temp ' 参照値を再セット
Debug.Write(mySlice(3).X) ' 51を返す
End Sub
End Module
Slice(Of T)が値型である場合、要素のメンバーを直接変更することはできません。その理由は、mySlice(3)から返される値がコピーであり、その要素への参照ではないからです。
' Doesn’t work. (動作しないよ)
mySlice(3).X += 1
値を書き換えたい場合には下記のように記述する必要があります。
これが出来るのは配列のインデクサーの戻り値が参照になっているためです。
参照:配列のインデクサーは参照戻り値
Dim temp = mySlice(3)
temp.X += 1
mySlice(3) = temp
この方法は値渡しによるコピーが発生することに加えて、インデックス作成操作が2回発生します。一度参照値を取得し、再度参照値をセットします。真の配列では指定インデックスの値をコピーせずに直接変更することが出来るので、特定の高性能アプリケーション(ゲーム等)では余計なオーバーヘッドは致命的となります。
また、ArraySegment(Of T)でも同様なことが出来るが、これも配列要素への直接アクセスに匹敵する性能はありません。理由の1つは、多くのコピーが必要になるためです。
Dim mySlice As New Slice(Of Point)(pt, 1)
↓
Dim mySlice = New ArraySegment(Of Point)(pt, 1, 5)
refによる解決
refを使用することで、基になる配列インデックスへの参照を直接返すスライスデフォルトプロパティ(またはC#のインデクサ)を書くことができます。
これがオーバーヘッドの全てを取り除くものです。インデックスは一度だけ発生し、その要素の値を変数に格納するか、その要素を別の値に設定しない限りはコピーは行われません。
参照:ref 戻り値 / ref ローカル変数 #インデクサで参照返し
最後に
何か分かったような分からないような感じですね。英語から日本語にGoogle翻訳り、InfoQの正式な日本語訳でも意味が分かりにくいんですよね。
「Visual Basic Language Design Meeting Notes - 2016.05.06」にあるように、現状は出来ないですが最終的には下記コードのような「ByRef」の使い方が可能となり、C#を経由しなくてもオーバーヘッドなく直接アクセス出来るようになるかと思われます。
' Signature of ArraySlice(Of Integer).Item
Default Public ReadOnly Property Item(index As Integer) As ByRef Integer
【追記 2017/06/24】
ようは、C#7の新し機能「ref 戻り値 / ref ローカル変数」を「ByRef」キーワードを使って出来るようにしたかったけど、C#とは微妙な機能な違いもあって同等性能にするにはまだ大変な作業があるから、現状はC#を経由した最小限の機能に留めておいたよってとこですね。