複雑な型とその周辺


アクセシビリティ

Nemerle のアクセシビリティは以下の 5 種類です。

アクセシビリティ修飾子 説明
public 任意の場所からアクセス可能
protected 宣言を含む型,あるいは宣言を含むクラスを継承したクラスからのみアクセス可能
internal 同一アセンブリ,もしくは InternalsVisibleToAttribute で指定されたアセンブリ内からのみアクセス可能
protected internal protected もしくは internal レベルでアクセス可能
private 宣言を含む型内でのみアクセス可能

アクセシビリティ修飾子を省略した場合,規定のアクセシビリティが適用されます。型の場合は internal,値や関数の場合は private になります。

名前空間

名前空間は複数の型を主に用途ごとに分類して管理するための機能です。

名前空間の定義は次のようにします。

namespace SomeNamespace
{
   public module SomeModule
   {
      public SomeFunction() : void {}
   }
}

名前空間はネストすることも可能です。

namespace AnotherNamespace
{
   namespace NestedNamespace
   {
      public module AnotherModule
      {
         public AnotherFunction() : void {}
      }
   }
}

名前空間内の型を参照するには, . 区切りで階層構造を指定します。

_ = AnotherNamespace.NestedNamespace.AnotherModule.AnotherFunction();

using ディレクティブ

名前空間を全て指定した修飾名を使うのを避けるために, using ディレクティブを使うことができます。

using AnotherNamespace.NestedNamespace;

_ = AnotherModule.AnotherFunction();

using ディレクティブで指定出来るのは名前空間のみでなく,型名まで指定可能です。

using AnotherNamespace.NestedNamespace.AnotherModule;

_ = AnotherFunction();

型名まで指定した場合,その型が公開する静的な値や関数にアクセスできます。

さて,同一の名前空間内に同一の名前の型は存在し得ませんが,異なる名前空間内であれば同一の名前の型が存在する場合があります。例えば次のような場合です。

namespace SomeNamespace { public module Foo {} }
namespace AnotherNamespace { public module Foo {} }

using ディレクティブで SomeNamespaceAnotherNamespace も参照している場合,単純に Foo と言った場合,いずれの名前空間の Foo を指すか曖昧になります。この場合完全修飾名を用いる方法もありますが, using を使って別名を付ける事ができます。これにより記述が簡潔になります。

using S = SomeNamespace;
_ = S.Foo.SomeFunction();  // SomeNamespace.Foo() と同じ

using AF = AnotherNamespace.Foo;
_ = AF.SomeFunction();

モジュール

モジュールは値や関数,あるいは他の型をまとめて外部に提供するための型です。 .NET Framework の静的クラスと同義です。

モジュールの定義は以下のような形式になります。

public module SomeModule
{
   public SomeValue = 1;
   public SomeFunction() : void {}
   public SomeInnerModule {}
}

クラス

クラスの定義は以下のような形式になります。

public class SomeClass
{
   public this()
   {
      base();
   }
   
   public this(_x : int)
   {
      this();
   }
   
   public event SomeEvent : System.EventHandler;
   
   public SomeProperty : string { get; set; }
   
   public SomeMethod() : void
   {
   }
}

this はコンストラクターで,クラスのインスタンスを作成する場合に呼び出されます。 1 つ目のコンストラクター内の base は継承元のクラスのコンストラクターの呼び出しです。省略した場合はデフォルトコンストラクター (パラメーターがないコンストラクター) が呼び出されます。 .NET Framework では全ての型は System.Object を継承しているので,この場合は System.Object クラスのコンストラクターを呼び出している事になります。 2 つ目のコンストラクター内の this は,別のコンストラクター (ここでは 1 つ目のコンストラクター) の呼び出しです。 basethis も,省略しない場合は,コンストラクター内の最初に呼び出さなければなりません。なお,コンストラクターの定義は省略できます。クラス定義内にコンストラクターが 1 つもない場合,デフォルトコンストラクターが定義されます (上の例のコンストラクターと同じ)。

コンストラクターを呼び出し,クラスインスタンスを作成するには次のようにします。

def something = SomeClass();  // SomeClass の this() が呼び出される

インターフェイスを実装したり,クラスを継承する場合は,クラス名の後ろに : を付けて実装,継承する型をカンマ区切りで列挙します。多重継承はできないので,インターフェイスは複数実装できますが,継承されるクラスは 1 つのみです。継承先のクラス (派生クラス) でオーバーライドされる可能性のあるプロパティやメソッドには継承元のクラス (基底クラス) で virtual キーワードを付け,派生クラスで override キーワードを付けてオーバーライドします。

public interface ISomeInterface
{
   InterfaceMethod() : void;
}
public class BaseClass
{
   public NonVirtualMethod() : void {}
   public virtual VirtualMethod() : void {}
}
public class DerivedClass : BaseClass, ISomeInterface
{
   public InterfaceMethod() : void {}
   // public override NonVirtualMethod() : void {}  // コンパイルエラー
   public override VirtualMethod() : void
   {
      System.Console.WriteLine("Overridden");
   }
}

abstract キーワードで抽象クラスを定義することができます。抽象クラスには,派生クラスで定義が実装される必要がある抽象プロパティや抽象メソッドを宣言できます。

public abstract class SomeAbstractClass
{
   protected this()
   {
   }
   
   public abstract SomeProperty : int { get; }
}

なお,抽象クラスのコンストラクターを省略した場合,自動実装されるデフォルトコンストラクターのアクセシビリティは protected になります。抽象クラスを継承して抽象プロパティや抽象メソッドの実装をする場合は, virtual と同様に override キーワードを付ける必要があります。

public class SomeDerivedClass
{
   private value = 0;
   public override SomeProperty : int { get { this.value; } }
}

クラスの継承を禁止する場合は sealed キーワードを用います。

public sealed class SomeClass
{
}
// 継承出来ないクラスを継承しようとしたためコンパイルエラー
// public class AnotherClass : SomeClass
// {
// }

なお, abstract キーワードと sealed キーワードは共存出来ません。前者が継承を要求するキーワードであるのに対し,後者は継承を禁止するキーワードなので当然ですね。

インターフェイス

インターフェイスは,それを実装する型が,インターフェイスの宣言するメンバーを持っている事を保証するための機構です。外部からは実装されたインターフェイスのメンバーが存在する事を期待されるため,インターフェイスを実装するクラスでは,インターフェイスで宣言されたメンバーのアクセシビリティは原則として public にしなければなりません。したがってインターフェイスにおいてはメンバーのアクセシビリティは指定する必要がありません。

インターフェイスの定義は以下のような形式になります。

public interface ISomeInterface
{
   event SomeEvent : EventHandler;
   SomeProperty : string { get; set; }
   SomeMethod() : void;
}

複数のインターフェイスを実装する場合,インターフェイスのメンバーの名前が衝突してしまう場合があります。この問題は,インターフェイスの明示的な実装により回避できます。インターフェイスの明示的な実装は implements キーワードを用いて次のように実装します。

using System.Console;

public interface ISomeInterface
{
   Foo() : void;
}
public interface IAnotherInterface
{
   Foo() : void;
}
public class ImplementedClass : ISomeInterface, IAnotherInterface
{
   SomeFoo() : void
      implements ISomeInterface.Foo
   {
      WriteLine("ISomeInterface.Foo");
   }
   AnotherFoo() : void
      implements IAnotherInterface.Foo
   {
      WriteLine("IAnotherInterface.Foo");
   }
}

def instance = ImplementedClass();
// instance.Foo();  // ImplementedClass のメンバーに Foo はないのでコンパイルエラー
(instance : ISomeInterface).Foo();  // ISomeInterface.Foo
(instance : IAnotherInterface).Foo();  // IAnotherInterface.Foo

明示的なインターフェイスの実装をした場合,そのメンバーのアクセシビリティは必ずしも public にする必要がありません。したがって明示的なインターフェイスの実装は,名前が衝突する場合の解決以外にも,インターフェイスには宣言されているが外部から呼ばれる意味がないメンバーを隠蔽する際にも役立ちます[A]

構造体

構造体はクラスと似ていますが,いくつかの点で異なります。

まず第一にクラスが参照型であるのに対して構造体は値型です。すなわち,割り当てられるメモリー領域が異なります。クラスのインスタンスは作成されるとヒープに割り当てられますが,構造体のインスタンスはスタックに積まれます。関数に渡される際にクラスインスタンスは参照渡しですが,構造体は値渡しです。例えば画面上の位置を表すといった軽量なデータ構造には構造体が向いています。

次にクラスは継承ができるのに対して構造体は継承ができません。ただし,インターフェイスの実装は可能です。

クラスはデフォルトコンストラクターをユーザーが定義することができますが,構造体はデフォルトコンストラクターが自動で実装されるために,ユーザーが独自の実装をすることができません。ただし,パラメーターを持つコンストラクターはユーザーが定義出来ます。

構造体の例を次に示します。

public struct Point
{
   public mutable X : int;
   public mutable Y : int;

   public this(x : int, y : int)
   {
      X = x;
      Y = y;
   }
   
   public override ToString() : string
   {
      System.String.Format("({0}, {1})", X, Y);
   }
}

列挙型

列挙型の定義は以下のような形式になります。

public enum Coins
{
   | One = 1
   | Five = 5
   | Ten = 10
   | Fifty = 50
   | Hundred = 100
   | FiveHundred = 500
}

列挙型の実体は整数値で,明示的に値を指定しない場合は 0 から 1 ずつ値が増えていきます。

列挙型の整数値の型は明示的に指定しなければ int になります。指定する場合は次のように指定出来ます。

public enum Coins : uint
{
   // ...
}

特に列挙型をフラグ管理に扱う場合は System.FlagsAttribute で注釈します。

using System;

[Flags]
public enum AvailableLanguages
{
   | None = 0b0000
   | Japanese = 0b0001
   | English = 0b0010
   | Chinese = 0b0100
   | Other = 0b1000
}

using AvailableLanguages;
def skill = Japanese %| English;

using ディレクティブ」で説明したように,公開されている静的メンバーへのアクセスを行う際には using で型名を指定することで型名の記述を省略できます。

バリアント

バリアントは列挙型と似ていますが,列挙型が整数値実体のプリミティブな値であるのに対し,バリアントは列挙される名前が,それぞれ値を持つことができます。

public variant Suit
{
   | Spade
   | Heart
   | Diamond
   | Club
}
public variant Card
{
   | Ace { Suit : Suit }
   | King { Suit : Suit }
   | Queen { Suit : Suit }
   | Jack { Suit : Suit }
   | Number { Suit: Suit; Rank : int }
   | Joker
}

using Suit;
using Card;
def aceOfHeart = Ace(Heart());
def nineOfDiamond = Number(Diamond(), 9);
def joker = Joker();

バリアントのインスタンスを作成する方法は,クラスや構造体と同様です。

前章に出てきたオプション型はバリアントの一例です。バリアントはオプション型と同様に,パターンマッチを使う際に非常に強力です。

型エイリアス

Nemerle では既存の型に別の名前 (エイリアス) を付ける事ができます。

public type int32 = System.Int32;
def x : int32 = 123;

エイリアスを付ける理由としては,外部ライブラリーの型名が,それを利用するライブラリーで使用される型名と一貫性がないという場合や,単純に簡潔な名前が欲しいという場合に使えば良いでしょう。 Nemerle のプリミティブ型が型エイリアスの実例です。

デリゲート

デリゲートは特定の型を持つ関数を統一的に扱うために名前を付ける仕組みです。型エイリアスとインターフェイスの中間的な性質を持っていると言えます。

public delegate Calculation(x : int, y : int) : int;
def add = Calculation(_ + _);
_ = add(1, 2);
def subtract : Calculation = _ - _;  // int * int -> int から暗黙に変換できる
// _ = subtract(1.0, 2.0);  // 型が一致しないのでコンパイルエラー

デリゲートは Nemerle 単体で開発するのであればほぼ使う必要のない機能ですが,他言語との相互運用の際に必要になってきます。

匿名型

匿名型はその名の通り型名のない型です。タプルのそれぞれの要素に名前を付けたような感覚です。

using Nemerle.Extensions;

def alice = new (Name = "Alice", Age = 23);
def bob = new (Name = "Bob", Age = 45);

この例で alicebob はいずれも NameAge という値を持つので,同一の型になります。型は一致しますが,匿名型はコンパイル時にコンパイラが自動で型を生成するため,コード中ではその型を知る術はありません。

コンパイル時に生成,ということで気付くかもしれませんが,匿名型はマクロです。匿名型を使うために必要なマクロが Nemerle.Extensions 名前空間に定義されているので,コード中で Nemerle.Extensions 名前空間を参照する必要があるのです。

匿名型と他言語の類似した機能の比較は『Nemerle, C#, F# における匿名型,レコードの比較』を参照下さい。

キャスト

しばしば変数の型と,その場で必要とする型が一致しないことが起こります。例えば次のような場合です。

public interface I { Method() : void; }
public class A : I { private RenamedMethod() : void implements I.Method {} }

def x = A()
// x.Method();  // コンパイルエラー

クラス A はインターフェイス I を実装するため I で定義された Method を呼ぶことができるはずですが, A でインターフェイスの明示的な実装で名前が変更されているため Method を呼ぶことができません。また,変更された名前のメソッドはアクセシビリティが private であるため A の型のまま呼ぶこともできません。

次のような場合もあります。

public class B {}
public class C : B { public Method() : void {} }

def y : B = C();
// y.Method();  // コンパイルエラー

y は明らかにクラス C のインスタンスですが,変数に付けられた型は B であるため, C で定義された Method を呼ぶことができません。

これらの問題は変数に付けられた型に起因します。したがって変数の型を正しい型に変換してやれば良いでしょう。このような型の変換をキャストと呼びます。キャストには 2 方向あります。 1 つ目は,前者の例のように,インターフェイスの実装クラスからインターフェイス型への変換や,あるいは派生クラスから基底クラスへの変換のように,変換先の型への変換が問題なく行える変換です。これをアップキャストと呼びます。 2 つ目は,後者の例のように,基底クラスから派生クラスからへの変換です。これはダウンキャストと呼ばれ,一般に安全な変換ではありません。なぜなら,例えば上の例で C 以外の B の派生クラスとして C と無関係な型 D があった場合, B からのダウンキャスト先は C 以外にも D が考えられるからです。

アップキャストとダウンキャストは異なる操作ですので,記法も異なります。アップキャストは : の後ろに型名を書いて,ダウンキャストは :> を書いて,それぞれの方向の型変換をします。上の 2 つの例ではそれぞれ次のように書けます。

(x : I).Method();
(y :> C).Method();

脚注

  1. 例えば IEnumerable[T] インターフェイスを実装するが, Reset メソッドを実装しない場合には Reset メソッドを public にする必要性はほとんどありません。 []

コメントを残す

メールアドレスが公開されることはありません。