10
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【PHP8.5】返り値を無視することを許さないNoDiscardアトリビュート

Last updated at Posted at 2025-03-24

PHPは元々型や引数まわりのシステムがアバウトだったこともあり、返り値を受け取らずに放棄する書き方がまかり通っていることがよくあります。

$fp = fopen('/tmp/lock.txt', 'r+');
flock($fp); // うごく
fwrite($fp, $text);
flock($fp, LOCK_UN);
fclose($fp);

まあPHP以外の言語でも普通に可能ですが。

ということで、帰り値の受け取りを強制するアトリビュート#[\NoDiscard]が提唱されました。
既に受理されており、PHP8.5から使用可能になります。

PHP RFC: Marking return values as important (#[\NoDiscard])

Introduction

現代のPHP APIで採用されるエラー処理メカニズムは例外です。
このメカニズムでは、失敗したときの処理を行わないかぎり非常に明白なエラーが発生します。
しかし、全てが成功もしくは失敗だけのどちらかだけではなく、部分的に成功というステータスを取りたい場合もあるでしょう。

この場合は返り値によって対応することができます。
しかしその場合、返り値をチェックしなければサイレントなエラーが起きてしまう可能性もあります。
APIによっては滅多にエラーが起きなかったり、非決定的な場合があるため、一見しただけでは問題に気付かないことがあります。

/**
 * 配列の全値を処理して返す。
 * 返り値は成功した項目はnull、失敗なら例外の入った配列。
 * 
 * @param array<string> $items
 * @return array<null|Exception>
 */
#[\NoDiscard("as processing might fail for individual items")]
function bulk_process(array $items): array {
	$results = [];
 
	foreach ($items as $key => $item) {
		if (\random_int(0, 9999) < 9999) {
			// 99.99%は成功するダミー処理
			echo "Processing {$item}", PHP_EOL;
			$error = null;
		} else {
			$error = new \Exception("Failed to process {$item}.");
		}
 
		$results[$key] = $error;
	}
 
	return $results;
}

Proposal

このRFCでは、関数およびメソッドの返り値が重要であり、返り値に何もしないことはバグであるとみなすアトリビュート#[\NoDiscard]を導入します。
以下、関数と言った場合は関数およびメソッドを意味します。

#[Attribute(Attribute::TARGET_METHOD|Attribute::TARGET_FUNCTION)]
final class NoDiscard
{
    public readonly ?string $message;
 
    public function __construct(?string $message = null) {}
}

#[\NoDiscard]のついた関数を呼び出す場合、エンジンは返り値が使用されていないときにはE_WARNINGもしくはE_USER_WARNINGを発行します。

『返り値を使用する』とは、返り値が他の式の一部であることを意味します。
意味のある式である必要はなく、ダミー変数に割り当てるだけでも使用したとみなされます。

(void) cast to suppress the warning

ただし、OPCacheは意味のない構文を最適化に伴って除去する可能性があります。
これによって警告が出てしまう可能性があります。
そこで本RFCは、返り値を意図的に使用しないことを表すvoid()キャストも提案します。
void()キャストは文であり、式ではありません。
式中で使用すると構文エラーが発生します。

Native functions where the attribute is applied

このRFCでは、以下のネイティブ関数にアトリビュートが適用されます。

flock
ロックを取得できなかったことを無視すると、データの破損が発生する可能性があります。
ロック失敗は負荷が高いときにのみ発生するので、テストで問題を発見することは困難です。

DateTimeImmutable::set*()
そもそも名前が不適切であり、オブジェクト自体の値は更新されることはなく、新しいインスタンスを返します。
この種のメソッドに確立された命名スキームはwith*()です。
この命名では、DateTimeからDateTimeImmutableに移行した際に返り値の使用を忘れる可能性があります。

Constraints

#[\NoDiscard]を追加すると、以下の場合にコンパイルエラーが発生します。

・関数の返り値の型として:void:neverが指定されている
・返り値がないマジックメソッド。__construct__clone

Examples

$items = [ 'foo', 'bar', 'baz' ];
 
// Warning: The return value of function bulk_process() is expected to be consumed, as processing might fail for individual items
bulk_process($items);
 
// エラー出ない。
$results = bulk_process($items);

// エラー出ない。
(void)bulk_process($items);
 
// エラー出ない。ただしOPCacheによって除去される可能性があり、その場合はエラーが出る。
(bool)bulk_process($items);
set_error_handler(static function (int $errno, string $errstr, ?string $errfile = null, ?int $errline = null) {
	throw new \ErrorException($errstr, 0, $errno, $errfile, $errline);
});

#[\NoDiscard("エラーメッセージ")]
function bulk_process(array $items): array {
	echo __FUNCTION__, PHP_EOL;
 
	return [];
}
 
function get_items(): array {
	echo __FUNCTION__, PHP_EOL;
 
	return [ 'foo', 'bar', 'baz' ];
}
 
try {
	// "get_items"
	bulk_process(get_items());
} catch (\Exception $e) {
	// "The return value of function bulk_process() is expected to be consumed, as エラーメッセージ"
	echo $e->getMessage(), PHP_EOL;
}
#[\NoDiscard]
function bulk_process(array $items): void { }  // Fatal errorになる。返り値はvoid

#[\NoDiscard]
function bulk_process(array $items): never { }  // Fatal errorになる。返り値はnever

Recommended usage

このアトリビュートは、関数のユーザが返り値の使用を忘れる可能性があり、返り値を使用しないと発見することが難しい問題の発生する可能性がある関数で使用することを目的としています。

上記のbulk_process()は適切な使用例です。
これは副作用のある関数であり、大抵は成功するので、普通のテストでは失敗例を見逃しやすくなります。

いっぽうstr_contains()に使うのは不適切です。
開発者がstr_contains()を使ったうえで結果に対して何もしないことは考えられません。
また本関数には副作用がなく、返り値の使用を忘れたときは単に不要な処理が行われるだけです。
同様に、ほとんどの純粋関数には必要ありません。

Precedent

先例。

PHP

PHPStorm IDE、PHPStan、Psalmは、純粋関数の返り値が使用されていないチェックをすでにサポートしており、DateTimeImmutable::set*()の落とし穴を検出可能です。
ただしbulk_process()に対しても検出できる診断は、RFC作成者の知るかぎりではありません。

Other programming languages

この機能と(void)キャストについては、C言語C++言語からインスパイアされています。

Rust#[must_use = "message"]構文をサポートしており、メッセージをサポートするきっかけになりました。

Javaは一部のエコシステムに@CheckReturnValueアノテーションがありますが、言語自体にはありません。

Swiftはデフォルトで警告を発します。
@discardableResultアトリビュートで、警告を抑えることができます。

Golangにはデフォルトで警告を発するようにする機能リクエストが存在します。

TypeScriptには機能リクエストが存在します。
ESLintにはPromiseが消化されているかチェックする機能が存在します。

Backward Incompatible Changes

グローバル名前空間でクラス名NoDiscardが使用できなくなります。
GitHub上には存在せず、影響はほぼありません。

(void)キャストは現在、(void)という名前の定数への有効なアクセスです。
GitHub上には存在せず、影響はほぼありません。

一部の関数は、安全でない方法で使用するとE_WARNINGを発生するようになります。
ダミー変数に割り当てることでエラーを抑制する回避方法は、古いバージョンのPHPでも確実に利用可能であるため、利点が互換性の問題点を上回ると考えています。

Proposed PHP Version(s)

PHP8.5

RFC Impact

To Opcache

新しいセマンティクスが追加されるため、OPCacheの変更が必要になります。
現在の実装は、JITを含めてすべてのテストを通過しています。

New Constants

ext/tokenizerに定数T_VOID_CASTが追加されます。

Future Scope

将来の展望は、今のところありません。

Proposed Voting Choices

可決には2/3の賛成が必要です。

本提案は、賛成18反対4の賛成多数で受理されました。

Patches and Tests

#[\NoDiscard] attribute
(void) cast

感想

ユーザへの影響としては、flockが一番大きいでしょう。
まあ対処も$_ = flock()ってするだけだから一瞬ですが。

自分で使うときは、どのようなときに使えばいいのかいまいちよくわかりません。
きっと誰かがよい使い方を教えてくれるはず。

10
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
10
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?