【Java】パターン変数のスコープの定義を "ちゃんと" 読む (JLS 6.3.)
はじめに
皆さんは、Java のパターンマッチ構文をご存じでしょうか?
void main() {
Object o = "hello!";
// o が文字列かどうかを判定
if (o instanceof String s) {
// String 変数 "s" が使用可能!!
IO.println(s);
}
}
あるインスタンスが子クラスであるかどうかを判定する構文ですが...
if (o instanceof String s) と書くと、その分岐では s が定義済みとなり、使用可能になります。
まぁ、別にこれ自体はあまり不自然な感じではありません。
でも、以下を見るとなんか変な感じを覚えないでしょうか?
void main() {
Object o = "hello!";
// o が文字列かどうかを判定
if (!(o instanceof String s)) {
// 判定に失敗したので、"s" は使えない
} else {
// ...こっちではString 変数 "s" が使用可能!!
IO.println(s);
}
}
instanceof の結果を否定すると、逆に else 節の方で s が有効になります。
論理的には確かに else で使えるのは妥当なのですが...
どういうロジックか気になりますよね!!!
そういう時は仕様を見ましょう。
Javaの仕様といえば、みなさんご存じ JLS (Java Language Specification) です。
本記事では、「パターンマッチにおける変数のスコープ」の仕様に絞って JLS を1文ずつ解釈・解説していきます。[1]
では、行ってみましょう。
テクニカルタームについて
さて、仕様を読む前に、JLS で使用されている、テクニカルターム[2]の定義を確認しないといけません。
JLS を引用しながら確認していきましょう。
introduced by - ~によって定義される
このテクニカルタームは、JLS 6.3 で以下のように触れられています。
The analysis relies on the technical term "introduced by", which has the following form:
- a pattern variable is introduced by an expression when true
- a pattern variable is introduced by an expression when false
- a pattern variable is introduced by a statement
明確に定義が与えられているわけではないですが...各文を訳すと以下になるはずです。
- a pattern variable is introduced by an expression when (it is) true
- あるパターン変数が、
expressionによって定義される
(expressionが true の場合に限り)
- あるパターン変数が、
- a pattern variable is introduced by an expression when (it is) false
- あるパターン変数が、
expressionによって定義される
(expressionが false の場合に限り)
- あるパターン変数が、
- a pattern variable is introduced by a statement
- あるパターン変数が、
statementによって定義される
- あるパターン変数が、
... と、つまりは 「expression や statement を書くことで、新たなパターン変数が定義される」という意味でしょう。
また、しばしば以下のような使われ方をします。
- a pattern variable introduced by a statement is ~
-
statementによって定義された あるパターン変数は ~
-
次のテクニカルタームと一緒に使用されがちですね。
is definitely matched (at ~) - (~の場所で) 絶対にマッチしたと確信する
これは少し定義が与えられています。
The scope of a pattern variable declaration (that is, a local variable declared by a pattern) is the part of the program that might be executed after the matching of a value against the pattern has succeeded (§14.30.2).
It is determined by considering the program points where the pattern variable is definitely matched in a region beginning with the pattern that declares the pattern variable.
訳出すると...
パターン変数 (パターンによって宣言されたローカル変数)のスコープは、値とパターンのマッチが成功した後に実行されるだろうプログラムの部分となる。
そのスコープは、パターン変数を定義したパターンから始まる領域のうち、そのパターン変数が絶対にマッチしたと確信される プログラムの地点を考慮することで決定される
ということです。明確には書いてないですが、
definitely matched である = パターン変数が使用可能である
と考えてよいでしょう。
また、 definitely matched at ~ で場所を指定する場合もあり、この場合は ~のプログラム地点においては、パターン変数が使用可能である という意味です。
なお、以降は 「~の場合に definitely matched である」 = 「~の場合にパターン変数が使用可能である」 という条件がたくさん書いてあるだけです。
6.3.1.1. Conditional-And Operator &&
本題に入りましょう。6.3.1.1 は && 演算におけるスコープの定義です。
The following rules apply to a conditional-and expression a && b (§15.23):
- A pattern variable introduced by a when true is definitely matched at b.
- A pattern variable is introduced by a && b when true iff either (i) it is introduced by a when true or (ii) it is introduced by b when true.
ここでは、a && b において、以下のようにパターン変数が使用可能であると言っています。
- 「a が true である場合に定義されるパターン変数」は、
bで使用可能となる- 例:
o instanceof String s && s.length() > 3はエラーにならない
- 例:
- 以下のすべてが「
a && bが true である場合に定義されるパターン変数」となる[3]-
aが true の場合に定義される、パターン変数 -
bが true の場合に定義される、パターン変数
-
- 「
a && bが false である場合に定義されるパターン変数」は存在しない
It is a compile-time error if any of the following conditions hold:
- A pattern variable is both (i) introduced by a when true and (ii) introduced by b when true.
- A pattern variable is both (i) introduced by a when false and (ii) introduced by b when false.
これはコンパイルエラーの定義で、
- 「a が true である場合に定義されるパターン変数」と「b が true である場合に定義されるパターン変数」(の識別子)が 一致しているならコンパイルエラー
- 「a が false である場合に定義されるパターン変数」と「b が false である場合に定義されるパターン変数」(の識別子) が 一致しているならコンパイルエラー
ということです。
後者の条件は不自然では?
前者は当然ですが、後者は若干不自然ですね。
なお、 || の場合にも同じ条件があります。
おそらくですが、意図的に考慮していない設計にしたのでしょう。
事実、パターン変数導入時の設計 では、以下のように記載があります。

For intersection, it is an error if any variables in the sets being intersected contain the same name but have different types.
当時はまだ同じ型だったらエラーにならない設計だったようです。
また、Java 15 で試験導入された時のプレビューJLS では、
this(注:このコンパイルエラー条件) may be relaxed in future versions of the language (provided the types are identical
と記載されています (正式導入では消えましたが。)
これ以上の調査では、設計から変わった経緯はわかりませんでしたが、
おそらくこの方がコンパイルが簡単なのでしょう。
6.3.1.2. Conditional-Or Operator ||
|| では、&& の条件がちょうど逆になります。
それだけなので、訳出だけ置いておき、原文は記載しません。
a || b では、以下のようにパターン変数が使用可能。
- 「a が false である場合に定義されるパターン変数」は、
bで使用可能となる- 例:
!(o instanceof String s) || s.length() > 3はエラーにならない
- 例:
- 以下のすべてが「
a || bが false である場合に定義されるパターン変数」となる[3:1]-
aが false の場合に定義される、パターン変数 -
bが false の場合に定義される、パターン変数
-
- 「
a || bが true である場合に定義されるパターン変数」は存在しない
そして以下の場合にコンパイルエラー
- 「a が true である場合に定義されるパターン変数」と「b が true である場合に定義されるパターン変数」(の識別子)が 一致しているならコンパイルエラー
- 「a が false である場合に定義されるパターン変数」と「b が false である場合に定義されるパターン変数」(の識別子) が 一致しているならコンパイルエラー
6.3.1.3. Logical Complement Operator !
これはパターン変数集合の真偽がひっくり返るだけなので簡単ですね。
A pattern variable is introduced by !a when true iff it is introduced by a when false.
A pattern variable is introduced by !a when false iff it is introduced by a when true.
訳すと以下のようになるでしょう。
- 「
!aが true である場合に定義されるパターン変数」は、「aが false である場合に定義されるパターン変数」である- 「
!aが false である場合に定義されるパターン変数」は、「aが true である場合に定義されるパターン変数」である
6.3.1.4. Conditional Operator ? :
三項演算子におけるパターン変数ですね。
まず最初の方は、使用可能なパターン変数の定義ですね。
The following rules apply to a conditional expression a ? b : c (§15.25):
- A pattern variable introduced by a when true is definitely matched at b.
- A pattern variable introduced by a when false is definitely matched at c.
a ? b : c において...
- b で「
aが true である場合に定義されるパターン変数」が使用可能になる - c で「
aが false である場合に定義されるパターン変数」が使用可能になる
直感的にも正しく感じますね。
=====
コンパイルエラーの定義は若干多いです。
It is a compile-time error if any of the following conditions hold:
- A pattern variable is both (i) introduced by a when true and (ii) introduced by c when true.
- A pattern variable is both (i) introduced by a when true and (ii) introduced by c when false.
- A pattern variable is both (i) introduced by a when false and (ii) introduced by b when true.
- A pattern variable is both (i) introduced by a when false and (ii) introduced by b when false.
- A pattern variable is both (i) introduced by b when true and (ii) introduced by c when true.
- A pattern variable is both (i) introduced by b when false and (ii) introduced by c when false.
多いので訳しながら表にまとめます。
▼ コンパイルエラーのパターン (各行ごとに、そのパターン変数集合に同じパターン変数が定義されているとエラーになる)
| a | b | c |
|---|---|---|
| true時のパターン変数 | trueとfalseすべてのパターン変数 | |
| false〃 | trueとfalseすべてのパターン変数 | |
| true〃 | true〃 | |
| false〃 | false〃 |
そもそもローカル変数が被ってしまってエラーになるパターンもあるので、
むしろ、三項演算子は一部の例を書いてもコンパイルエラーにならず、それ以外はすべてエラーになる ... と理解した方がよいでしょう。
void main() {
Object o1 = "local1";
Object o2 = "local2";
boolean b = true;
// コンパイルエラーにならない例
var v1 = b ? (!(o1 instanceof String s)) : (o2 instanceof String s);
var v2 = b ? (o1 instanceof String s) : (!(o2 instanceof String s));
// 上記のような2パターン以外はコンパイルエラーになる!!
var v3 = b ? (o1 instanceof String s) : (o2 instanceof String s);
}
なぜこんな複雑な仕様にしたんだ??
a ? b : c は (a && b) || ((!a) && c) と型的には同じになります。なので、コンパイルエラーの条件も同じにしよう ... とするのは自然な発想でしょう。
void main() {
Object o1 = "local1";
Object o2 = "local2";
boolean b = true;
// コンパイルエラーにならない例
var v1 = b ? (!(o1 instanceof String s)) : (o2 instanceof String s);
// ... が以下と型的には等価 (こっちもエラーにならない)
var _v1 = (b && (!(o1 instanceof String s))) || (!b && o2 instanceof String s);
}
... が、それを満たさない場合もあります。
void main() {
Object o1 = "local1";
Object o2 = "local2";
boolean b = true;
// コンパイルエラーになる!!
var v1 = b ? (!(o1 instanceof String s)) : (!(o2 instanceof String s));
// ... が以下と型的には等価 (こっちはエラーにならない!!!)
var _v1 = (b && (!(o1 instanceof String s))) || (!b && (!(o2 instanceof String s)));
}
正直なところ、なぜこのような仕様なのかはあまりわかりません。
三項演算子は、外側にはパターン変数を導入しないので、変数名が衝突するような場合はないと思うのですが???
これは別の考察を与えて記事にするかもしれません。
6.3.1.5. Pattern Match Operator instanceof
この式は、そもそもパターン変数の発生源ですね。
The following rule applies to an instanceof expression with a pattern operand, a instanceof p (§15.20.2):
- A pattern variable is introduced by a instanceof p when true iff the pattern p contains a declaration of the pattern variable (§14.30.1).
A pattern variable is not permitted to shadow another local variable (§6.4).
和訳を与えると、
パターン
pの中で宣言したパターン変数は、「a instanceof pが true である場合に定義されるパターン変数」となるパターン変数は、他のローカル変数をシャドーする(同名にする)ことはできない
ですね。
これは今までのと比べればわかりやすいでしょう。コンパイルエラー条件もないですし。
6.3.1.6. switch Expressions
switch 式には二つのパターンがあり、それぞれ違うパターン変数が定義されます。(JLS 14.11.1)
// Switch rules (->)
String s = switch(a){
case 0 -> "zero";
default -> "other";
}
// Switch labeled statement groups (:)
String s = switch(a){
case 0: yield "zero";
default: yield "other";
}
▼ Switch rules (->) の場合
The following rule applies to a switch expression with a switch block consisting of switch rules (§14.11.1):
- A pattern variable introduced by a switch label is definitely matched in the associated switch rule expression, switch rule block, or switch rule throw statement.
Switch labelによって定義されたパターン変数は、それに紐づくブロックや式で使用可能になる
これは目玉機能なので直感的にもわかります。
var len = switch (o1) {
case String s -> s.length(); // sが使用可能!
default -> -1;
};
ただ...
▼ Switch labeled statement groups (:) の場合
The following rules apply to a switch expression with a switch block consisting of switch labeled statement groups (§14.11.1):
- A pattern variable introduced by a switch label is definitely matched in all the statements of the associated switch labeled statement group.
- A pattern variable introduced by a statement S contained in a switch labeled statement group is definitely matched at all the statements following S, if any, in the switch labeled statement group.
Switch labelによって定義されたパターン変数は、それに紐づくブロックや式で使用可能になるswitch labeled statement group内で、ある文によって定義されたパターン変数は、そのswitch labeled statement group内の後続の文によって使用可能になる (あれば)
... こちらはどうでしょうか。前者はわかりますが、後者は若干冗長な気がします。
が、これは Switch labeled statement groups が処理をブロックで書かなくても問題ないことが理由でしょう。
var len = switch (o1) {
case "aaa" -> { // 文は block で書くことが強制される
if (! (o2 instanceof String s)){
yield -1;
}
yield s.length(); // sが使用可能!
}
default -> -1;
};
var len = switch (o1) {
case "aaa": // 文を並べるだけでも良い
if (! (o2 instanceof String s)){
yield -1;
}
yield s.length(); // sが使用可能!
default:
yield -1;
};
-> の方では複数の文は block で書くことが強制されるので、block 文 によるパターン変数の定義 (JLS 6.3.2.1. Blocks) が使用可能となります。なので、わざわざここに書く必要はないといったところでしょう。
代わりに、: の場合は 文を block せずに並べるだけでよいので、わざわざ定義を追加しないといけなかったのでしょう。多分。
6.3.1.7. Parenthesized Expressions
式に () をつけた場合ってことなのでほぼ自明です。
The following rules apply to a parenthesized expression (a) (§15.8.5):
- A pattern variable is introduced by (a) when true iff it is introduced by a when true.
- A pattern variable is introduced by (a) when false iff it is introduced by a when false.
- 「
(a)が true である場合に定義されるパターン変数」は、「aが true である場合に定義されるパターン変数」である- 「
(a)が false である場合に定義されるパターン変数」は、「aが false である場合に定義されるパターン変数」である
6.3.2.1. Blocks
ここからは 「式」ではなく 「文」 の話になります。
これは block 文の話ですね。
The following rule applies to a block statement S contained in a block (§14.2) that is not a switch block (§14.11.1):
- A pattern variable introduced by S is definitely matched at all the block statements following S, if any, in the block.
訳すと...
以下のルールが、
switch blockではない block 内の block 文に適用される。
- そのblock文によって定義されたパターン変数は、該当 block 内の後続の文で使用可能になる(後続があれば)。
先に補足しておくと、 block と block statement (block 文) は少し違う概念です。一部に block を含む文が block文 となるわけで、単に block というと メソッドの本体なども含みます。 (詳しくは JLS 14.2 などを参照しましょう)
まぁその辺を無視して端的に言うと、 例えば、直前の if 文によって定義されたパターン変数が後続で使用可能になる...と言っています。 (if は block 文なので)
6.3.2.2. if Statements
本記事で一番重い所となります。
The following rules apply to a statement if (e) S (§14.9.1):
- A pattern variable introduced by e when true is definitely matched at S.
- A pattern variable is introduced by if (e) S iff (i) it is introduced by e when false and (ii) S cannot complete normally.
訳すと、
if(e) S文には以下のルールが適用される
- e が true の時に定義されるパターン変数が、Sで使用可能になる
- S が 通常終了することができない 場合「
if (e) Sによって定義されるパターン変数」は、「eが false である場合に定義されるパターン変数」となる。
前者の定義はわかりやすいのでそのまま受け取りましょう。
が、後者の定義では見慣れない言葉があります。そうです、「通常終了することができない 場合」 とは何か ... という話です。
端的に言うと S 内で 例外が必ず投げられる場合などに相当します。
void main() {
String o1 = "local1";
if (!(o1 instanceof String s)) {
// 必ず例外が投げられるため、通常終了することができない
throw new RuntimeException("Unexpected type");
}
// s が使用可能!!
IO.println("s: " + s);
}
ただでさえ長いこの記事がさらに冗長になるので詳しくは以下の折り畳みにまとめます。
「通常終了することができる」とは?
「通常終了することができる」とは、JLS 14.22 によって定義されるテクニカルタームで、処理が次の文に移行する可能性がある ことを言います。逆に、「通常終了することができない」は、処理が次の文に移行する可能性は全くない ことを言います。各文の様式ごとに条件がいろいろ書いてありますが、詳しくはそれを見ません。
ただし、一番重要なのはおそらく以下なのでこれを見てみましょう。
A break, continue, return, throw, or yield statement cannot complete normally.
break や continue などは、次の処理に移行することは絶対にありません。
if とこれを組み合わせればパターン変数を使った早期リターンができますね。
ところで ある Twitter の投稿 では、以下のようなコードだと直感と反する結果になるということが言われています。(これはプレビュー版ですが、Java 25 時点ではそのまま引き継がれています。)
/*?final?*/ boolean FLAG = true;
String v = "field";
void main() {
String obj = "Pattern match";
if (!(obj instanceof String v)) {
// This branch is never taken.
while (FLAG) ; // endless
}
IO.println(v);
}
このコードでは、FLAG に final をつけると 出力は "Pattern match" になり、つけないと "field" になります。finalの有無によってパターン変数が使用可能かどうかが変わるわけです。
これは while が通常終了する条件(JLS 14.22.)が影響しているので、まずそれを確認しましょう。
A while statement can complete normally iff at least one of the following is true:
- The while statement is reachable and the condition expression is not a constant expression (§15.29) with value true.
(後略)
そうですね、「while 文の条件が true と評価される定数式でない限り、それは通常終了する」と示されています。後続の条件には今回の場合は当てはまらないので、この条件だけ勘案すると、逆に「while 文の条件が true と評価される定数式ならば、それは通常終了しない」といえます。while (FLAG) が通常終了するかどうかは FLAG が 定数かどうかで変わるわけです。
結果、if 文の後続で使用可能な パターン変数も変わるわけですね。
if-then-else の場合も似たようなものです。
The following rules apply to a statement if (e) S else T (§14.9.2):
- A pattern variable introduced by e when true is definitely matched at S.
- A pattern variable introduced by e when false is definitely matched at T.
A pattern variable is introduced by if (e) S else T iff either:
- It is introduced by e when true, and S can complete normally, and T cannot complete normally; or
- It is introduced by e when false, and S cannot complete normally, and T can complete normally.
if(e) S else T文には以下のルールが適用される
- e が true の時に定義されるパターン変数が、Sで使用可能になる
- e が false の時に定義されるパターン変数が、Tで使用可能になる
- T が 通常終了することができない 場合「
if (e) S else Tによって定義されるパターン変数」は、「eが true である場合に定義されるパターン変数」を持つ。- S が 通常終了することができない 場合「
if (e) S else Tによって定義されるパターン変数」は、「eが false である場合に定義されるパターン変数」を持つ。
6.3.2.3. while Statements
while 文です。
The following rules apply to a statement while (e) S (§14.12):
- A pattern variable introduced by e when true is definitely matched at S.
- A pattern variable is introduced by while (e) S iff (i) it is introduced by e when false and (ii) S does not contain a reachable break statement for which the while statement is the break target (§14.15).
訳すと
while (e) S文には以下のルールが適用される
- e が true の時に定義されるパターン変数が、Sで使用可能になる
- その while 文が、自分自身を break 先とする 到達可能な break 文を持たない限り、 「
while (e) Sによって定義されるパターン変数」は、「eが false である場合に定義されるパターン変数」を持つ。
前者はいいとして、後者は少し条件が厄介ですね。
ただ、以下のようなプログラムを考えればすぐに意図が分かります。
void main() {}
void ok() {
Object obj = "Pattern match";
while (!(obj instanceof String s)) {
}
// obj は絶対に String なので s が使用可能
IO.println(s);
}
void ng() {
Object obj = "Pattern match";
while (!(obj instanceof String s)) {
break;
}
// エラー!:break でここに来る可能性もあるので、s は使用不可
IO.println(s);
}
void butThisWorks() {
Object obj = "Pattern match";
label:
while (true) {
while (!(obj instanceof String s)) {
break label;
}
// エラー!:break でここに来る可能性は絶対にないので、s は使用可能
IO.println(s);
}
}
6.3.2.4. do Statements
do-while 文は、while 文から定義を絞っただけなのでシンプルです。 ( S は条件に関わらず必ず1回は実行されるため)
The following rule applies to a statement do S while (e) (§14.13):
- A pattern variable is introduced by do S while (e) iff (i) it is introduced by e when false and (ii) S does not contain a reachable break statement for which the do statement is the break target (§14.15).
- その do-while 文が、自分自身を break 先とする 到達可能な break 文を持たない限り、 「
do S while (e)によって定義されるパターン変数」は、「eが false である場合に定義されるパターン変数」を持つ。
6.3.2.5. for Statements
for も while 文とほぼ同じです。
The following rules apply to a basic for statement (§14.14.1):
- A pattern variable introduced by the condition expression when true is definitely matched at both the incrementation part and the contained statement.
- A pattern variable is introduced by a basic for statement iff (i) it is introduced by the condition expression when false and (ii) the contained statement, S, does not contain a reachable break for which the basic for statement is the break target (§14.15).
和訳:
通常のfor文には以下のルールが適用される
- 「forの条件式の、それが true の時に定義されるパターン変数」が、増加処理やforの内容文で使用可能になる
- その for 文が、自分自身を break 先とする 到達可能な break 文を持たない限り、 「通常のfor文 によって定義されるパターン変数」は、「条件式 が false である場合に定義されるパターン変数」を持つ。
もう一度言いますが、while と全く同じなのですよ。
ただ、増加部分でも使えるので、以下のような書き方が可能です。
void main (){
Object o = "aaa";
for (
int i = 0;
o instanceof String s && !s.isEmpty(); // 条件式
o = s.substring(1) // 増加部分 - sが使用可能!
) {
IO.println(s);
}
}
後に少し拡張forがあります。
An enhanced for statement (§14.14.2) is defined by translation to a basic for statement, so no special rules need to be provided for it.
拡張for は通常のfor への変換処理が定義されているので、ここでは特別なルールは定義しない。
なんかもったいぶった言い方ですが、拡張for に対応する通常forの条件式は、<イテレータ>.hasNext() のみなので (JLS 14.14.2) 、定義されるパターン変数は結局存在しません。
6.3.2.6. switch Statements
switch 文 は switch 式とほぼ同じなのであまり見る意味がありませんが、念のため和訳だけ置いておきます。
▼ Switch rules (->) の場合
The following rule applies to a switch statement with a switch block consisting of switch rules (§14.11.1):
- A pattern variable introduced by a switch label is definitely matched in the associated switch rule expression, switch rule block, or switch rule throw statement.
Switch labelによって定義されたパターン変数は、それに紐づくブロックや式で使用可能になる
(switch expression が switch statement に置き換わっただけ)
▼ Switch labeled statement groups (:) の場合
The following rules apply to a switch statement with a switch block consisting of switch labeled statement groups (§14.11.1):
- A pattern variable introduced by a switch label is definitely matched in all the statements of the associated switch labeled statement group.
- A pattern variable introduced by a statement S contained in a switch block statement group is definitely matched at all the statements following S, if any, in the switch block statement group.
Switch labelによって定義されたパターン変数は、それに紐づく文で使用可能になるswitch labeled statement group内で、ある文によって定義されたパターン変数は、そのswitch labeled statement group内の後続の文によって使用可能になる (あれば)
こっちもほとんど「式」が「文」に変わっただけです。
6.3.2.7. Labeled Statements
最後に、ラベル文という少し聞きなじみのない文です(...が、実は while 文の説明の時に出てきています)。
The following rule applies to a labeled statement (§14.7):
- A pattern variable is introduced by a labeled statement L: S (where L is a label) iff (i) it is introduced by the statement S, and (ii) S does not contain a reachable break statement for which the labeled statement is the break target (§14.15).
- その ラベル付けされた文が、自分自身を break 先とする 到達可能な break 文を持たない限り、 「
L: Sによって定義されるパターン変数」は、「Sによって定義されるパターン変数」を持つ。
これは以下のようなコードのことを言っています。
void main (){
Object o = "aaa";
label:
if (!(o instanceof String s)) {
// ここが return の場合、コンパイルエラーにはならない
// (o instanceof String s) が必ず true になるため
return;
// ここが break の場合、コンパイルエラーにはなる
// (o instanceof String s) が必ず true だとは限らないため
break label;
}
IO.println(s);
}
とはいえ、 ループ 文以外を break することなどほぼないので、このルールを適用することはまぁないでしょう。
おわりに
疲れた... (
ここまで読み終えた酔狂な方はぜひ評価でも押していってください。
Discussion