コンテンツにスキップ

D言語/関数

出典: フリー教科書『ウィキブックス(Wikibooks)』

関数

[編集]

関数とは一連の動作ををまとめた手続きであり、0個以上の引数を受け取り、1つの戻値を返すことができます。(C言語の関数についてはC言語/関数

コード例
import std.stdio;

void main()
{
    writeln("main内");
    auto n = 21;
    n.foo().writeln; // writeln(foo(n)); と同じ
    bar();
}

int foo(int n)
{
    writeln("foo内");
    return 2 * n;
}

void bar()
{
    writeln("bar内");
}
実行結果
main内
foo内
42
bar内

関数の定義・呼び出し方

[編集]

関数の戻値型の型推論

[編集]

関数の戻値型に、キーワード auto を使うことで、戻値型を型推論することができます。

コード例
import std.stdio;

void main()
{
    writeln(foo(2) + 4);
}

auto foo(int i)
{
    return i + 3;
}
実行結果
9
int 型の変数と int 型のリテラル同士の加算なので int が型推論されます。

スコープブロック文

[編集]

D言語では、関数の内部における変数の有効範囲(スコープ)の性質が、C言語とはやや異なります。 ブロック文( { ... } で囲まれた部分)はスコープブロック文と呼ばれ、スコープブロックは変数や関数などのユーザー定義のシンボルが見える範囲の区切りを表します。また、 スコープブロックは入れ子にすること可能です。

コード例
import std.stdio;

void main()
{
    func0();
    writef("aはmainでいま%d\n", a);
}

int a = 99;

void func0()
{
    int a = 2; // 9行の a は、トップレベルなので シャドウイング にあたらない。
    writef("aはfunc0でいま%d\n", a);
    // 新たなスコープ
    {
        // int a = 42; ← 13行目の a を隠してしまう(シャドウイング)ので a を識別子に使えない。
        writef("aはfunc0内の内側のスコープでいま%d\n", a);
    }
    writef("aはfunc0内の内側のスコープでいま%d\n", a);
}
実行結果
aはfunc0でいま2
aはfunc0内の内側のスコープでいま2
aはfunc0内の内側のスコープでいま2 
aはmainでいま99

内部関数

[編集]

基本

[編集]

D言語では、関数の内部で関数を定義でき、利用できます。D言語では、ほぼあらゆる言語の要素がいたるスコープで記述でき、例えば、import ですらも特定のスコープ内のみに適用する、といったことが可能です。内部関数は、それのほんの一例です。

コード例
import std.stdio;

void main()
{
    int x = 0;
    
    // ↓ これが内部関数
    void inner()
    {
        x++;
        writeln("inside! X = ", x);
    }
    // 内部関数はここで終わり

    writeln("1回目");
    inner();

    writeln("2回目");
    inner();
}
実行結果
1回目
inside! X = 1
2回目
inside! X = 2
標準C言語とC++には、2020年のいまのところ内部関数はありません。C#には2017年ごろ、C#7にて内部関数が追加されました(なお、C#の内部関数の記法はD言語のそれとは大きく違う)。

内部関数と変数の有効範囲について

[編集]

内部関数の中にある変数の扱いは、通常のスコープとは異なり、識別子が重複するとより内側の関数スコープの識別子が使われ、シャドウイングとしてエラーにはなりません。

内部関数と内包する関数のローカル変数の識別子と衝突した場合

[編集]
コード例
import std.stdio;

void main()
{
    outer();
    writef("mainに帰還。aはいま%d\n", a);
}

int a = 7;

void outer()
{
    int a = 5;
    void inner()
    {
        int a = 3;
        writef("内部関数の中で aはいま%d\n", a);
    }

    inner(); // 利用の際は呼び出すのを忘れないように
    writef("関数では aはいま%d\n", a);
}
実行結果
内部関数の中で aはいま3
関数では aはいま5
mainに帰還。aはいま7
関数スコープの識別子の解決は、スコープの内側から外側に向かってトップレベルまで行なわれます。
トップレベルまで探査して見つからなければ、未定義変数の参照となります。
これは、コンパイル時に静的に行なわれます。
他の言語の経験がある人であれば「クロージャー」だと言えば判りやすいでしょうか。

内部関数からトップレベルの変数を参照

[編集]

いっぽう、再宣言しない場合、外側のスコープにある変数を書き換えます。

内部関数から内包する関数のローカル変数を参照
import std.stdio;

void main()
{
    outer();
    writef("mainに帰還。aはいま%d\n", a);
}

int a = 7;

void outer()
{
    void inner()
    {
        a = 3; // int が無く、再宣言なしの単なる代入命令
        writef("内部関数の中で aはいま%d\n", a);
    }

    inner(); // 利用の際は呼び出すのを忘れないように
    writef("関数では aはいま%d\n", a);
}
実行結果
内部関数の中で aはいま3
関数では aはいま3 
mainに帰還。aはいま3
3行目のaの値に注目してください。上記コードの内部関数では再宣言しないで代入したので、mainに帰還した際にも、変数aが7でなく3に置き換わっています。

セキュリティ・レベル

[編集]

C言語に無い特徴として、D言語の「関数」には

  • @system
  • @trusted
  • @safe

の3種類のセキュリティ・レベルがあります。

何も指定しない場合、レベルは @system になっています。

int foo() @system
{
    return 0;
}

のように指定します。

@system で宣言された関数(ディフォルト)
C言語の関数のように、全ての言語機能にアクセスできます。
@safe で宣言された関数
ポインタの利用が禁止されます。
@safe または @trusted な関数だけしか呼び出しできません。
(@system レベルの関数を呼び出しできません)
コード例
import std.stdio;

void main(){
      writeln( foo() );
}

int foo() @system
{
    return 28;
}
実行結果
28
うごくコード例2
import std.stdio;

void main(){
      
      writeln( foo() );
}

int foo2() @safe
{
    return 35;
}

int foo() @safe
{
    return foo2();
}
実行結果
35
禁止されているコード例(コンパイルエラー)
import std.stdio;

void main()
{
    writeln(foo());
}

int foo2() @system
{
    return 35;
}

int foo() @safe
{
    return foo2();
}
コンパイル結果
onlineapp.d(15): Error: `@safe` function `onlineapp.foo` cannot call `@system` function `onlineapp.foo2`
onlineapp.d(8):        `onlineapp.foo2` is declared here


契約プログラミング

[編集]

基本

[編集]

関数を定義する際、入力値の要求事項と、出力値の要求事項とを記述する事ができる。また、自身が記述したアルゴリズムがバグがなく動作した場合に成り立つべき事項を確かめることができる。この仕組みを「契約」と言う。

D言語の契約プログラミングでは、要求事項を見たさない入力または出力がされた際、プログラムの実行を停止する。また、静的な契約 (static assert) も存在し、こちらはコンパイル時間数実行が可能な範囲においてコンパイル時にチェックされ、条件が満たされない場合はコンパイルがエラーとなる。

コード例(自然数に対して階乗を計算するプログラム)
import std.stdio;

void main()
{
    writeln(factorial(5));
}

int factorial(int n)
in
{
    assert(n >= 0); // 階乗への入力は非負整数でなければならない
}
out (result)
{
    assert(result >= n); // n! ≧ n
    assert(n <= 3 || result > n ^^ 2); // n ≧ 4 のとき、n! > (nの2乗)
}
do
{
    if (n == 0)
        return 1;
    return n * factorial(n - 1);
}
実行結果
 
120
解説や書式
書式
in 
{
    assert(入力の要求事項);
}
out (result)
{
    assert(出力の要求事項);
}
do
{
    return 出力内容;
}
のように書く。
例として、もし writeln( factorial(4) ); で、引数を4でなく、たとえば「-7」など負の数にすると、入力の要求事項を満たさなくなるので、実行時エラーになる。
エラーの例
 
core.exception.AssertError@onlineapp.d(12): Assertion failure
----------------
??:? _d_assertp [0x56223707d810]
./onlineapp.d:12 int onlineapp.factorial(int) [0x56223707c3fe]
./onlineapp.d:6 _Dmain [0x56223707c3ca]
2022年現在、C言語には、直接契約プログラミングをサポートしてはいないが、assert()関数とマクロセットで契約プログラミングを実現する試みがある、

契約プログラミングを公式にサポートしている言語はいまのところ少ない。(非標準のライブラリなどでサポートされている言語はそこそこあるが、しかしプログラム言語の機能としてサポートされている言語は、かなり少ない。)

契約の使い方に関する注意

[編集]

契約をユーザーの入力のチェックに使ってはいけない。契約とは、あくまでも自身が書いたアルゴリズム、コードの正しさをチェックするためのみに存在するものである。関数の定義域を制限したい場合は、Assertion Error ではなく例外やエラーメッセージを残すべきである[1]

悪い例

[編集]

readf() などの関数を使うと、ユーザーによるキーボードからの入力を受け付ける。それを使って数値をいろいろと入力して、契約プログラミングのコードをテストしてみるとする。

コード例
import std.stdio;

void main()
{
    writeln("Please input number");

    int some;
    readf("%d", &some);

    writef("you input %d\n", some);
    writeln(bbb(some));
}

float bbb(float num)
in
{
    assert(num >= 0);
}
out (result)
{
    assert(result >= 0);
}
do
{
    return num + 3;
}

実際にテストしてみると、何も入力していない間は、いったんコンパイルできて実行できても、「-4」などマイナスの数を入力すると、そこで実行を停止する。 (コマンド rdmd でも コマンド dmd でも同様。)

なお、契約違反の発見の際、

core.exception.AssertError@hello.d(5): Assertion failure

のようにエラーメッセージが表示される。

もちろん、プラスの「5」など契約に適合した数値を入れているかぎりは、動作する。

これは、「契約をユーザーの入力のチェックに使ってはいけない」という決まりに反しており、契約の誤った使い方である。もちろん、決まりを破ってでも契約によるエラーメッセージを出したい理由があるならばそうすることもできるが、特段の理由がない限りは避けるべきである。

註:この例がファジングを意図していると解すべきではない。

参考文献

[編集]
  1. ^ Andrei Alexandrescu 著, 長尾 高弘 (翻訳) 『プログラミング言語D』翔泳社 2013年 第一版 10章「契約プログラミング」より