次の方法で共有


Null 許容型

Sophia Salim SDET (Visual Studio マネージ言語グループ)

2008 年 3 月

概要

Null 許容型 は、Visual Studio 2008 で導入された新機能です。この機能は、一貫性のある方法を使用して、値型と参照型で Null と何もない状態を表現することを目的としています。簡単に言えば、特別な方法で宣言された値型にはリテラル "Nothing" が含まれるようになりました。このホワイトペーパーでは、値型を Null 許容型として宣言する方法、Null 許容型として宣言された変数を使用する方法、および Null 許容型と他の言語機能との相互関係について説明します。

対象製品:

Visual Studio 2008

はじめに

初級レベル

Null 許容型の定義

構文

単純な変換と演算

メンバへのアクセス

上級レベル

変換

演算子

リフト

Null 許容型と If 演算子

まとめ

付録 1

はじめに

このホワイトペーパーは、次の 2 つのセクションで構成されています。

初級レベル : このセクションでは、Null 許容型を使用する方法、構文、およびメンバについて簡単に説明します。

上級レベル : このセクションでは、Null 許容型を他の言語構造体と組み合わせて使用する方法について説明します。また、Null 許容型に対して実行される演算子のリフトのしくみや、Null 許容型を新しい If 演算子と組み合わせて使用する方法についても説明します。

初級レベル

Null 許容型の定義

Null 値は、リテラル Nothing を使用して値が存在しないことを表現する、型システムの中でも特殊な値です。Null 値は、どの型にでも代入できるわけではありません。許容される値の範囲に Null 値が含まれる型を "Null 許容型" といいます。

当然、参照型はすべて Null 許容型です。Visual Studio 2008 では、次の 2 種類の値型が提供されるようになりました。

  • Null 許容の値型
  • Null 非許容の値型

現在 VB で提供されているすべての値型は、Null 非許容の値型です。Visual Studio 2008 では、このような値型にそれぞれ対応する Null 許容型が導入されました。たとえば、値型 Integer に対応する Null 許容型は "Integer?" です。Integer 型には値 -2147483647 ~ 2147483648 を格納できますが、Integer? には値 -2147483647 ~ 2147483648 だけでなく、Null 値 Nothing も格納できます。

組み込みの値型とユーザー定義の値型 (構造体と列挙体) は、どちらも Null 許容型として宣言できます。Null 許容型として定義できる組み込みの型については、付録 1 を参照してください。

構文

次のコードでは、Null 許容型を定義するさまざまな方法を紹介しています。

    '組み込みの Null 許容の値型
    Dim  i As Integer?
    Dim j? As Integer
    Dim k As Nullable(Of Integer)

同様に、ユーザー定義の値型の場合も、次のように同じ構文を使用します。

    Public Structure s1
        Dim mem As Integer
    End Structure
    Sub Main()
        'ユーザー定義の Null 許容の値型
        Dim i As s1?
        Dim j? As s1
        Dim k As Nullable(Of s1)
    End Sub

次のように、Null 許容型の配列を宣言することもできます。

    Dim i As Integer?()
    Dim j?() As Integer
    Dim k As Nullable(Of Integer)()

単純な変換と演算

Null 値を Null 非許容の値型に変換すると、その Null 値は暗黙的に、変換先となる値型の 0 に相当する値に変換されます。ある型の 0 値とは、その型の変数で何もない状態を表すときに使用する値の範囲に含まれる値です。たとえば、値 Nothing を Integer 型に変換すると、その値はなくなるのではなく、数値 0 に変換されます。

Null 許容型に対して実行されたすべての演算では、連結の場合を除いて、Null が伝播します (詳細については、「演算子」の「連結演算子」を参照してください)。Null 伝播演算を実行すると、いずれかのオペランドが Null 値であった場合、結果は Null 値になります。Null 非伝播演算を実行すると、Null 値が格納されたオペランドは、その演算の型の 0 値に変換され、代わりに変換後の値が使用されます。

次のコードでは、Null 伝播演算を実行し、Null 許容型の Integer に対して + 演算子を使用しています。

    Dim i As Integer?
     i = i + 1 'Null 伝播演算であるため、結果は Nothing です

メンバへのアクセス

各 Null 許容型 T? では、それぞれの値の読み取りとテストに使用できる、次の 3 つのメンバが既定で提供されます。

  • Value: Null 許容変数の値を、基になる型 T として取得します。
  • GetValueOrDefault: Null 許容変数の値を取得します。Nothing の場合は、基になる型 T の既定値を返します。
  • HasValue: 変数に値と Nothing のどちらが格納されているかに基づいて、true または false を返します。

次のコードでは、これら 3 つのメンバを使用しています。

 Dim i As Integer?
        Console.WriteLine(i.HasValue)
        Console.WriteLine(i.GetValueOrDefault)
        Try
            Console.WriteLine(i.Value)
        Catch ex As InvalidOperationException
            Console.WriteLine("Exception: Cannot access value if nullable variable is nothing")
        End Try
        i = 1
 Console.WriteLine(i.HasValue)
 Console.WriteLine(i.GetValueOrDefault)
 Console.WriteLine(i.Value)

上級レベル

変換

T と T? との間の変換

値型 T と対応する Null 許容型 T? について考えてみましょう。T と T? との間の変換には、次の規則が適用されます。

  • T には T? への上位変換が定義されています。
  • T? には T への縮小変換が定義されています。
  • 変換する値が Nothing である場合に T? から T への変換を実行すると、System.InvalidOperationException 例外がスローされます。

変換のサンプルを次に示します。

 Dim i1 As Integer = 1
 Dim i2? As Integer
 Try
 i1 = i2
 Catch ex As InvalidOperationException
 'Null 許容変数には Nothing が格納されているため、この例外が発生します
 End Try
 '上位変換
 i2 = i1 'i2 に値 1 が格納されます
 '縮小変換。Option Strict が On になっている場合はコンパイル エラーが発生します
 i1 = i2 'Null 許容型に値が格納されているため有効です

他の型との間の変換

T と S という 2 つの型について考えてみましょう。T には型 S への変換 (上位変換または縮小変換) が定義されています。T、S、およびそれぞれに対応する Null 許容型の間の変換には、次の規則が適用されます。

  • T? から S? への同じ種類の変換 (上位変換または縮小変換) が可能です。
  • T から S? への同じ種類の変換 (上位変換または縮小変換) が可能です。
  • S? から T への縮小変換が可能です。
  • T? が Nothing である場合に T? から S? への変換を実行すると、結果は Nothing になります。
  • S? が Nothing である場合に S? から T への変換を実行すると、System.InvalidOperationException 例外が発生します。

Integer 型と Long 型との間の変換のサンプルを次に示します。

  'Integer(T) には Long(S) への上位変換が定義されています
      Dim i1 As Integer
      Dim i2 As Long
      Dim null_i1? As Integer
      Dim null_i2? As Long
        null_i2 = null_i1 '有効な上位変換。null_i2 の値は
                              'Nothing になり、例外はスローされません
         null_i2 = i1 '有効な上位変換
         i1 = null_i2 '縮小変換
        null_i2 = Nothing
        Try
            i1 = null_i2
        Catch ex As InvalidOperationException
             'null_i2 が Nothing であるため、この例外が発生します
        End Try

ボックス化と Null 許容値

Null 許容の値型 T? をボックス化する、つまり Object、Object?、System.ValueType、System.ValueType? のいずれかに変換すると、型 T? ではなく T の値がボックス化されます。これは、参照型がもともと Null 許容型であり、Null 許容型への変換を必要としないためです。その結果、T? をボックス化解除すると、その値はボックス化された値型 T のインスタンスまたは Nothing に "ラップ解除" されます。逆に、Null 許容の値型 T? をボックス化解除すると、その値は Nullable(Of T) によって "ラップ" され、Nothing は型 T? の Null 値にボックス化解除されます。

次に例を示します。

  'この関数は、基になる値が Nothing の場合でも
  'Null 許容変数の型を返します
  
  Public Function CheckType(Of T)(ByVal arg As T) As System.Type
      Return GetType(T)
  End Function
  Sub Main()
      Dim i1? As Integer = Nothing
      Dim o1 As Object = Nothing
      
     Console.WriteLine(CheckType(i1)) 'System.Nullable<System.Int32> を出力します
      Console.WriteLine(o1 Is Nothing) 'True を出力します
      i1 = 1
     o1 = i1 '初期化されていないオブジェクトに対して gettype を呼び出すと例外が返されるため、この処理が必要です
      Console.WriteLine(o1.GetType().ToString()) 'System.Int32 を出力します
      Dim i2 = CType(o1, Integer?)
     Console.WriteLine(CheckType(i2).ToString()) '10 を出力します
  End Sub

この動作が引き起こす二次的な問題は、Null 許容の値型 T? が T のすべてのインターフェイスを実装するように見えることです。この問題が発生する理由は、値型をインターフェイスに変換するには、その型をボックス化する必要があるためです。その結果、T? は T の変換先として有効なすべてのインターフェイスに変換できますが、Null 許容の値型 T? には、実際には一般的な制約チェックやリフレクションを行う T のインターフェイスは実装されません。このしくみを理解しておくことは重要です。

ユーザー定義変換

型では、対応する Null 許容型や他の型との間のユーザー定義変換を宣言できます。たとえば、次の宣言は有効です。

    Structure T
        ...
    End Structure

    Structure S
        Public Shared Widening Operator CType(ByVal v As S?) As T
            ...
        End Operator
    End Structure

ただし、有効性を確保する目的で、まず、変換の宣言に含まれている型のすべての "?" 修飾子が削除され、その後、従来の型チェックがユーザー定義変換に適用されます。この型チェックは、"特定の型の変換は、その型と別の型との間の変換でない限り許可されない" という規則に基づいて実行されます。たとえば、構造体 S では S から S (同じ型) への変換を定義できないので、次の宣言は無効です。

 Structure S
 Public Shared Widening Operator CType(ByVal v As S) As S?
 ...
 End Operator
 End Structure

演算子

定義済み演算子

任意の型 T が定義済み演算子 op と共に使用され、op が連結演算子ではない場合、次の規則が T? に適用されます。

  • op は T? に対して定義されます。
  • T? のオペランドが Nothing ではない場合、T? を op と共に使用した結果は、T を op と共に使用した結果と等しくなります。
  • T? のオペランドが Nothing である場合、T? を op と共に使用した結果は Nothing になります。
  • いずれかのオペランドが Null 許容型である場合、結果の型は Null 許容型になります。

次に例を示します。

  Dim v1? As Integer = 10
 Dim v2 As Long = 20
 Console.WriteLine(v1 + v2) ' 演算の型は Long? になります

連結演算子

連結演算子は、Null 許容型の Null を伝播させない特殊な演算子です。他の参照型と同様、Null 許容変数に値 Nothing が格納されている場合、その値が空の文字列に変換されてから、連結が実行されます。次のコードは、この動作を示しています。

  Dim t As Integer?
 Console.WriteLine("This string is concatenated with a null Integer?" & t)
 t = 1
 Console.WriteLine("The value of nullable variable is: " & t)

この動作により、他の参照型と Null 許容型との間の一貫性が保証されます。また、Null 許容型に Null が格納されている場合、その値をユーザーがメッセージ ボックスやコンソールで確認したときに、例外がスローされなくなります。

ブール式

定義済み演算子には、擬似演算子 IsTrue と IsFalse があります。これらの演算子は Boolean? に対する演算に使用できるように拡張されています。つまり、Boolean? はブール式内で使用できます。値が Nothing である場合、ブール式の結果は True にも False にもなりません。次に例を示します。

 Dim x? As Boolean
 x = Nothing
 If x Then
 '実行されません
 End If

つまり、ショートサーキット論理演算子 AndAlso と OrElse では、最初のオペランドが値 Null を保持する Boolean? である場合、2 番目のオペランドが評価されることになります。次に例を示します。

 Dim x?, y? As Boolean
 x = Nothing
 y = False

 'x と y の両方が評価されます
 If x AndAlso y Then
 '実行されません
 End If

Boolean? に対して使用される論理演算子 And と Or の定義は、この 3 つの値を持つ Boolean のロジックに対応するように拡張されました。この定義を次に示します。

  • And のオペランドがどちらも True である場合、結果は True と評価され、いずれかが False である場合は False と評価されます。それ以外の場合は Nothing と評価されます。
  • Or のいずれかのオペランドが True である場合、結果は True と評価され、オペランドがどちらも False である場合は False と評価されます。それ以外の場合は Nothing と評価されます。

次に例を示します。

 Dim x?, y? As Boolean

 x = Nothing
 y = True

 If x Or y Then
 '実行されます
       End If

ユーザー定義演算子

型では、変換と同様、対応する Null 許容型を使用するユーザー定義演算子を宣言できます。たとえば、次の宣言は有効です。

 Structure T
 Dim mem As Integer
 End Structure

 Structure S
 Dim mem As Integer
 Public Shared Operator +(ByVal v1 As S?, ByVal v2 As T) As T
 End Operator
 End Structure

ただし、有効性を確保する目的で、まず、演算子の宣言に含まれている型からすべての ? 修飾子が削除され、その後、従来の型チェックがユーザー定義変換に適用されます。この規則は、演算子によって明示的に要求される次の型には適用されません。

  • シフト演算子の 2 番目のパラメータは Integer? ではなく Integer である必要があります。
  • IsTrue と IsFalse では Boolean? ではなく Boolean が返される必要があります。

リフト

Null 許容型自体にはメンバは存在しません。これらのメンバは、基になる型からリフトできます。Null 許容型で基になる型のメンバを取得し、そのメンバを Null 非許容型から Null 許容型に昇格させる処理を "リフト" といいます。

リフトされた変換

ユーザー定義の Null 許容型 T? を他の任意の型 (S や対応する Null 許容型の S? など) に変換するときには、次の手順が使用されます。

  1. T? に関連するユーザー定義変換が存在する場合は、その変換が他のすべての変換候補に優先されます。
  2. 変換する値が Nothing である場合は、どの変換も呼び出されず、Nothing が返されます。
  3. T? の変換を解決する際には、T に関連するすべてのユーザー定義変換が候補として使用されます。
  4. T から S への変換は T? から S? への変換にリフトされます。まず、型 T? の引数が型 T に変換されます。
  5. ユーザー定義の変換演算子が評価され、結果として型 S が返されます。
  6. この結果が型 S? に変換されます。

手順 2. ~ 5. で、リフトの処理を定義しています。次のコードは、これらの手順を示しています。

  Module Test
   Public result As String
   Structure S
   Dim i As Integer
   End Structure

   Structure T
   Dim i As Integer
   Public Shared Widening Operator CType(ByVal v As T) As S
   result = "Conversion was called"
   Return New S With {.i = v.i}
   End Operator
   End Structure
   Sub Main()
   Dim x As T?
   Dim y As S?

   result = Nothing
   y = x '手順 2: 何も返されず、変換は呼び出されません
   Console.WriteLine(If(result, "Conversion was not called"))

   result = Nothing
   x = New T With {.i = 1}
   y = x '手順 3. ~ 6.
   Console.WriteLine(If(result, "Conversion was not called"))
   Console.WriteLine("Was coversion successful? Answer: " & (y.Value.i = x.Value.i))
      End Sub
   End Module

リフトされた演算子

ユーザー定義の Null 許容型 T? に対する演算を実行するときには、次の手順が使用されます。

  1. T? にユーザー定義演算子が存在する場合は、その演算子が他のすべての候補に優先されます。
  2. T? に対する演算を解決する際には、Null 非許容のオペランドを必要とする、T のすべてのユーザー定義演算子も候補として使用されます。
  3. いずれかのオペランドが Nothing である場合、結果は Nothing になり、その結果の型は予期される型の Null 許容型になります。
  4. Null 非許容のオペランドを必要とする演算子が選択された場合、その演算子はリフトされます。つまり、すべてのオペランドと結果の型は対応する Null 許容型に変換されます。
    特定の型であることが必要なオペランドや結果 (シフト演算子の 2 番目のパラメータや IsTrue または IsFalse 演算子の戻り値など) は、対応する Null 許容型には変換されません。
  5. オペランドが対応する Null 非許容型に変換され、リフトされた演算子が評価されます。
  6. リフトされた演算子内で定義されている演算が実行されます。
  7. 結果の型が、対応する Null 許容型に変換されます。

次のコードは、この処理を示しています。

  Module Test
      Public result As String
      Structure S
          Dim i As Integer
      End Structure
      
      Structure T Dim i As Integer
          Public Shared Operator +(ByVal v1 As T, ByVal v2 As S) As S
              result = "Operator was called"
              Return New S With {.i = v1.i + v2.i}
          End Operator
   End Structure

   Sub Main()
   Dim x As T?
   Dim y As S?

   result = Nothing
   y = x + y
   Console.WriteLine(If(result, "Operator was not called"))
          Console.WriteLine("Was operation successful? Answer: " & (y Is Nothing))

   result = Nothing
   x = New T With {.i = 1}
   y = New S With {.i = 1}
   y = x + y
   Console.WriteLine(If(result, "Operator was not called"))
          Console.WriteLine("Was operation successful? Answer: " & (y.Value.i = 2))
   End Sub
  End Module

Null 許容型と If 演算子

If 演算子が 2 項演算に対応するようになったため、Null 許容型と参照型が扱いやすくなりました。この演算子には 2 つのオペランドを指定できますが、最初のオペランドには Null 許容型と参照型のどちらかを指定する必要があります。2 つのオペランド間には上位変換が定義されている必要があり、2 つのうち上位のオペランドの型が結果の型になります。2 番目のオペランドが Null 非許容型である場合、式の結果の型を決定するときに、最初のオペランドの型から ? が削除されます。

次のコードでは、2 項演算に対応した If 演算子を Null 許容型と共に使用しています。

 Dim i As Integer?
 Dim j = If(i Is Nothing, 10, i) '3 項演算子を拡張して
                                 '2 項演算子と同じ結果を返します
 Dim k = If(i, 10) '2 項演算子を使用します。3 項演算子を使用した場合の余分なコードが
                    'ほとんどなくなります

次のコードは、Null 許容型のオペランドを使用した場合に、結果の型がどのようにして決定されるかを示しています。

Dim a As Integer?
    Dim b As Long?
    
    Dim i = If(a, 10) '結果の型を決定するときに ? が a から
                         '削除され、i の型は Integer になります
    
    Dim j = If(a, b) '結果の型を決定するときも ? が保持され、 
                       'j の型は Long? (2 つのうち上位の型) になります

付録 1

Null 許容型として定義できる組み込みの値型を次に示します。

ByteSByteShortIntegerLongUShortUIntegerULongSingleDoubleBooleanCharDate

まとめ

Null 許容型 は、Visual Studio 2008 で導入された新機能です。この機能は、一貫性のある方法を使用して、値型と参照型で Null と何もない状態を表現することを目的としています。簡単に言えば、特別な方法で宣言された値型にはリテラル "Nothing" が含まれるようになりました。このホワイトペーパーでは、値型を Null 許容型として宣言する方法、Null 許容型として宣言された変数を使用する方法、および Null 許容型と他の言語機能との相互関係について説明しました。

Sophia Salim は、マイクロソフトの Visual Studio マネージ言語グループで SDET を担当しています。