関数パラメータ
この章では、さまざまな種類の関数パラメータについて説明する。
この章の概念の一部は、この本の前半で既に紹介している。例えば、foreach
ループの章で見たref
キーワードは、foreach
ループで、要素のコピーではなく実際の要素を使用できるようにするものだった。
さらに、const
およびimmutable
キーワード、値型と参照型の違いについても、前の章で説明した。
パラメータを利用して結果を生成する関数を作成した。例えば、次の関数は、そのパラメータを計算に使用している。
この関数は、小テストの成績の40%と期末試験の成績の60%を平均して、平均成績を計算する。この関数の使用方法は次の通りだ。
パラメーターは常にコピーされる
上記のコードでは、2つの変数がweightedAverage()
の引数として渡されている。この関数は、そのパラメータを使用している。この事実から、この関数は引数として渡された実際の変数を使用しているとの誤解を招くかもしれない。実際には、この関数が使用しているのは、それらの変数のコピーである。
この区別は、パラメータを変更するとそのコピーのみが変更されるため、重要である。これは、パラメータを変更しようとしている(つまり、副作用を生じさせようとしている)次の関数で確認できる。ゲームキャラクターのエネルギーを減らすために、次の関数が記述されていると仮定しよう。
reduceEnergy()
をテストするプログラムは以下の通りだ:
出力:
新しいエネルギー: 100 ← 変更なし
reduceEnergy()
は、そのパラメータの値を元の値の4分の1に減少させるが、main()
内の変数energy
は変化しない。これは、main()
内の変数energy
とreduceEnergy()
のパラメータenergy
は別物であり、パラメータはmain()
内の変数のコピーであるためだ。
これをより詳しく確認するために、writeln()
式をいくつか挿入しよう。
出力:
関数を呼び出す : 100
関数に入った : 100
関数を離れる : 25 ← パラメータが変更された、
関数から戻った: 100 ← 変数は変わらない
参照されている変数はコピーされない
スライス、連想配列、クラス変数などの参照型のパラメータも関数にコピーされる。ただし、参照されている元の変数(スライスや連想配列の要素、クラスオブジェクトなど)はコピーされない。事実上、このような変数は参照として関数に渡される。つまり、パラメータは元のオブジェクトへの別の参照になる。これは、参照を通じて変更を行った場合、元のオブジェクトも変更されることを意味する。
文字のスライスであるため、これは文字列にも適用される:
パラメーターの最初の要素に変更を加えると、main()
内の実際の要素にも影響する:
.bc
ただし、元の切り出しと連想配列の変数はコピーで渡される。パラメーター自体がref
として修飾されていない場合、予期しない結果が生じる可能性がある。
スライスの意外な参照セマンティクス
スライスとその他の配列機能の章で見たように、スライスに要素を追加すると要素の共有が終了する場合がある。当然、共有が終了すると、上記のstr
のようなスライスパラメーターは、渡された元の変数の要素への参照ではなくなる。
例えば、次の関数によって追加された要素は、呼び出し元からは見えない。
この要素は、関数パラメータにのみ追加され、元のスライスには追加されない。
appendZero()の内部 : [1, 2, 0]
appendZero()が返った後: [1, 2] ← No 0
新しい要素を元の切り出しに追加する必要がある場合は、切り出しをref
として渡す必要がある:
ref
修飾子は以下で説明する。
連想配列の意外な参照セマンティクス
関数パラメータとして渡される連想配列も、連想配列はnull
として、空の状態ではなく開始するため、予期しない動作をする可能性がある。
この文脈では、null
は初期化されていない連想配列を意味する。連想配列は、最初のキーと値のペアが追加されると自動的に初期化される。その結果、関数がnull
の連想配列に要素を追加した場合、その要素は元の変数では見ることができない。これは、パラメータは初期化されるものの、元の変数はnull
のままになるためだ。
元の変数には追加された要素は存在しない:
appendElement()の内部 : ["red":100]
appendElement()が返った後: [] ← まだnull
一方、連想配列が最初にnull
でなかった場合、追加された要素は呼び出し元からも参照できる:
この場合、追加された要素は呼び出し元からも見える:
appendElement()の内部 | ["red":100, "blue":10] |
---|---|
appendElement()が返った後 | ["red":100, "blue":10] |
そのため、連想配列をref
パラメーターとして渡す方が良いかもしれない。これについては後で説明する。
パラメーター修飾子
パラメータは、上記の一般的な規則に従って関数に渡される。
- 値型はコピーされ、その後、元の変数とコピーは独立したものになる。
- 参照型もコピーされるが、参照型の性質上、元の参照とパラメータの両方が同じ変数にアクセスする。
これらは、パラメーター定義に修飾子が指定されていない場合のデフォルトのルールだ。以下の修飾子は、パラメーターの渡し方や、それらに対して許可される操作を変更する。
in
デフォルトでは、in
パラメーターはconst
パラメーターと同じだ。これらは変更できない:
ただし、‑preview=in
コンパイラコマンドラインスイッチを使用すると、in
パラメータは、"このパラメータはこの関数によってのみ入力として使用される"という意図を表現するのに、より有用になる。
-preview=in
in
パラメーターの意味を変更し、in
パラメーターに引数を渡す際にコンパイラーがより適切なメソッドを選択できるようにする:
in
の意味は、const scope
を意味するように変更される(scope
については以下を参照)。ref
パラメータとは異なり、r値もin
パラメータとして渡すことができる(ref
については以下、r値については次の章を参照)。- コピーすると副作用が生じる型(コピーコンストラクタが定義されている場合など)や、コピーできない型(コピーコンストラクタが無効になっている場合など)は、参照で渡される。
‑preview=in
コマンドラインスイッチの使用有無に関わらず、const
パラメーターよりもin
パラメーターを使用することをおすすめする。
out
関数は、生成した結果を戻り値として返すことはご存じの通りだ。戻り値が1つしかないことは、1つの関数で複数の結果を生成する必要がある場合、制限となることがある。(注釈:戻り値の型をTuple
またはstruct
と定義することで、複数の結果を返すことができる。これらの機能については、後の章で説明する。)
out
キーワードを使用すると、関数はパラメータを通じて結果を返すことができる。out
パラメータが関数内で変更されると、その変更は関数に渡された元の変数にも反映される。ある意味で、割り当てられた値はout
パラメータを通じて関数から出力される。
2つの数値を割り、商と余りを両方生成する関数を見てみよう。戻り値は商に使用され、余りはout
パラメータを通じて返される。
関数のremainder
パラメータを変更すると、main()
のremainder
変数が変更される(名前は同じである必要はない)。
結果 | 余り |
---|---|
2 | 1 |
呼び出しサイトでの値に関係なく、out
パラメータは、まずその型の.init
値に自動的に割り当てられる。
関数内でパラメータに明示的な代入がない場合でも、パラメータの値は自動的にint
の初期値となり、main()
内の変数に影響を与える。
関数呼び出し前 : 100
関数に入った後 : 0 ← int.initの値
関数から戻った後: 0
この例からわかるように、out
パラメータは関数に値を渡すことはできない。これらは、関数から値を渡すためだけに使用される。
後の章で、out
パラメータの代わりにTuple
またはstruct
型を返すほうが良いことを説明する。
const
const
パラメーターよりもin
パラメーターを使用することをおすすめする。
前述のように、const
は、関数内でパラメータが変更されないことを保証する。プログラマにとっては、特定の変数が関数によって変更されないことを知っておくと便利だ。const
は、const
、immutable
、および変更可能な変数をそのパラメータとして渡すことを可能にするため、関数の有用性を高める。
immutable
前述のように、immutable
は、関数に対して特定の変数を不変にすることを要求する。この要件により、次の関数は、immutable
要素(文字列リテラルなど)を含む文字列でのみ呼び出すことができる。
immutable
はパラメータに要件を強制するため、不変性が要求される場合にのみ使用すべきだ。それ以外の場合は、immutable
、const
、および変更可能な変数を受け入れるconst
の方が一般的に有用だ。
ref
このキーワードを使用すると、通常はコピー(つまり値)として渡される変数を、参照として渡すことができる。
r値(次の章を参照)は、ref
パラメータとして関数に渡すことはできない。
先ほど見た、元の変数を変更するreduceEnergy()
関数では、そのパラメータをref
として受け取る必要がある。
この場合、パラメータに対する変更は、main()
で元の変数にも反映される:
新しいエネルギー | 25 |
---|
このように、ref
パラメータは入力と出力の両方として使用できる。ref
パラメータは、元の変数の別名とも考えることができる。上記の関数パラメータenergy
は、main()
内の変数energy
の別名だ。
out
パラメータと同様に、ref
パラメータも関数に副作用を持たせることができる。実際、reduceEnergy()
は値を返しさない。単一のパラメータを通じて副作用を引き起こすだけである。
関数型プログラミングと呼ばれるプログラミングスタイルでは、副作用よりも戻り値が優先されるため、一部の関数型プログラミング言語では副作用がまったく使用できない。これは、戻り値によってのみ結果を生成する関数は、理解、実装、および保守が容易であるためだ。
同じ関数は、副作用を引き起こす代わりに結果を返すことで、関数型プログラミングスタイルで記述することができる。プログラムの変更部分は強調表示されている。
関数の名前の変更にも注意。動詞ではなく、名詞になった。
auto ref
この修飾子はテンプレートでのみ使用できる。次の章で見るように、auto ref
パラメータはl値を参照で、r値をコピーで受け取る。
inout
名前が"in
"と"out
"から成っているにもかかわらず、このキーワードは入力と出力を意味しない。入力と出力は、ref
キーワードで実現されることは既に説明した。
inout
は、パラメータの変更可能性を戻り値の型に伝える。パラメータがconst
、immutable
、またはmutableの場合、戻り値もそれぞれconst
、immutable
、またはmutableになる。
inout
がプログラムでどのように役立つかを確認するために、パラメータの内部要素にスライスを返す関数を見てみよう。
出力:
[6, 7, 8]
この本でこれまで説明してきたことに従って、この関数をより有用なものにするためには、そのパラメータはconst(int)[]
であるべきだ。これは、関数内で要素が変更されないためだ。(パラメータのスライス自体は元の変数のコピーなので、それを変更しても問題はないことに注意しよう。)
しかし、そのように関数を定義すると、コンパイルエラーが発生する。
このコンパイルエラーは、const(int)
のスライスは、変更可能な int
のスライスとして返すことができないことを示している。
戻り値の型をconst(int)[]
と指定すれば解決すると思われるかもしれない。
これでコードはコンパイルされるようになったが、制限がある。関数が変更可能な要素のスライスで呼び出された場合でも、返されるスライスはconst
要素で構成されることになる。この制限がどれほど厳しいかを確認するために、スライスの内部要素を変更しようとする次のコードを見てみよう。
const(int)[]
型の返されたスライスは、int[]
型のスライスに代入できないため、エラーになる。
しかし、変更可能な要素のスライスから始めたため、この制限は人為的で不運なものとなっている。inout
は、パラメータと戻り値の間のこの変更可能性の問題を解決する。これは、パラメータと戻り値の両方の型で指定され、前者の変更可能性を後者に引き継ぐ。
この変更により、同じ関数をconst
、immutable
、および変更可能なスライスで呼び出すことができるようになった。
lazy
引数を使用する関数に入る前に、引数が評価されることは当然のことだ。例えば、以下の関数add()
は、他の2つの関数の戻り値で呼び出される。
add()
が呼び出されるためには、まずanAmount()
とanotherAmount()
が呼び出されなければならない。そうしないと、add()
が必要とする値が利用できなくなってしまう。
関数を呼び出す前に引数を評価することを、熱心な評価と呼ぶ。
しかし、特定の条件によっては、一部のパラメータが関数内でまったく使用されない場合がある。そのような場合、引数を熱心に評価することは無駄になる。
この状況の典型的な例は、メッセージの重要度が特定の設定値以上である場合にのみメッセージを出力するロギング関数だ。
例えば、ユーザーがLevel.high
のメッセージのみに関心がある場合、Level.medium
のメッセージは出力されない。しかし、関数を呼び出す前に引数は評価される。例えば、以下のformat()
式は、それ自体が呼び出すgetConnectionState()
呼び出しも含めて、メッセージが決して出力されない場合、すべて無駄になってしまう。
lazy
キーワードは、パラメータとして渡された式は、必要な場合にのみ評価されることを指定する。
この場合、message
パラメータが使用された場合にのみ、式が評価される。
注意すべき点は、lazy
パラメータは、その関数内でそのパラメータが使用されるたびに評価されることだ。
例えば、次の関数のlazy
パラメータは関数内で3回使用されているため、その値を提供する式は3回評価される。
出力:
計算中...
計算中...
計算中...
3
scope
このキーワードは、パラメータが関数のスコープ外では使用されないことを指定している。この記事の執筆時点では、scope
は、関数が@safe
と定義され、-dip1000
コンパイラスイッチが使用されている場合にのみ有効である。DIPはD改善提案の略だ。DIP1000はこの記事の執筆時点では実験的な機能であるため、すべての場合で期待どおりに動作するとは限らない。
上記の関数は、2つの点でscope
の約束に違反している。パラメータをグローバル変数に代入し、それを返している。これらの動作により、関数の終了後もパラメータにアクセス可能になってしまう。
shared
このキーワードは、パラメーターが実行スレッド間で共有可能であることを要求する:
上記のプログラムは、引数がshared
でないためコンパイルできない。コンパイル可能にするための必要な変更は以下の通りだ:
shared
キーワードについては、後述のデータ共有と並行処理の章で説明する。
return
関数が、そのref
パラメータの1つを直接返すことが便利な場合がある。例えば、次のpick()
関数は、そのパラメータの1つをランダムに選択して返し、呼び出し元が幸運なパラメータを直接変更できるようにする。
その結果、main()
内のa
またはb
のいずれかに、42
の値が割り当てられる。
a | b |
---|---|
42 | 0 |
a | b |
---|---|
0 | 42 |
残念ながら、pick()
の引数の1つは、返される参照よりも寿命が短い場合がある。例えば、次のfoo()
関数は、2つのローカル変数を使用してpick()
を呼び出し、事実上、そのうちの1つへの参照を返す。
a
とb
の両方の有効期間はfoo()
を離れると終了するため、main()
での代入は有効な変数に対して行うことができない。その結果、未定義の動作が発生する。
未定義の動作とは、プログラミング言語の仕様ではプログラムの動作が定義されていない状況を指す。未定義の動作を含むプログラムの動作については、何も言うことはできない。(ただし、実際には、上記のプログラムでは、値42
は、a
またはb
のいずれかが占めていたメモリ位置に書き込まれる可能性が高く、そのメモリ位置は現在、無関係な変数の一部となっている可能性があり、その結果、その無関係な変数の値が破損する可能性がある。
return
キーワードをパラメーターに適用することで、このようなバグを防止できる。これは、パラメーターが返される参照よりも長いライフタイムを持つ変数への参照でなければならないことを指定する:
この場合、コンパイラは、pick()
の引数の寿命が、foo()
が返そうとしている参照の寿命よりも短いことを認識する。
注釈:コンパイラがreturn
キーワードがなくてもpick()
を検査してバグを検出することは考えられるが、一部の関数の本体は、すべてのコンパイルでコンパイラが利用できない場合があるため、通常はそうはいかない。
要約
- パラメータは、関数がそのタスクを実行するために呼び出し元から受け取るものだ。
- 引数は、関数にパラメータとして渡される式(変数など)だ。
- すべての引数は、デフォルトではコピーによって渡される。(参照型の場合、コピーされるのは参照であり、元の変数ではないことに注意。)
in
パラメーターがデータ入力専用であることを指定する。const
よりもin
を優先して使用。out
パラメーターがデータ出力のみに使用されることを指定する。ref
パラメーターがデータ入力とデータ出力の両方に使用されることを指定する。auto ref
はテンプレートでのみ使用される。引数がl値の場合、その参照が渡されることを指定する。引数がr値の場合、コピーで渡される。const
パラメータが関数内で変更されないことを保証する。(const
は推移的であることを覚えておいてほしい。const
変数を通じて到達するデータは、const
でもある。)const
よりもin
を使うように。immutable
引数がimmutable
である必要がある。inout
パラメータと戻り値の型に両方出現し、パラメータの変更可能性を戻り値の型に伝達する。lazy
パラメーターが実際に使用されたとき(および毎回)に評価されるようにする。scope
パラメータへの参照が関数から漏れることがないことを保証する。shared
パラメータがshared
である必要がある。return
パラメータに指定すると、そのパラメータは返される参照よりも長く存続する必要がある。
演習
次のプログラムは、2つの引数の値を交換しようとしている。
しかし、このプログラムはa
やb
には何の影響も与えていない:
1 2 ← 入れ替わっていない
a
とb
の値が入れ替わるように、関数を修正しよう。