Зачем в rust if expression, возвращающий не unit type, оборачивать в скобки, чтобы оно вело себя как expression и не создавало ошибки компиляции?

Рейтинг: 0Ответов: 2Опубликовано: 06.07.2023

В синтаксисе rust все управляющие конструкции типа if, циклы типа for являються выражениями(expressions - expr). То есть они всегда, неявно или явно, что-то возвращают

Так же в rust инструкции (statements) могут быть декларативными и вычислительными (состоящими из expr). В конце каждой инструкции должна быть точка запятой

Из вышеуказанного можно заметить, что конструкцию if нужно завершать ;, потому что конструкция if это expr. И нет "особой" группы statements, которые могли бы не завершаться ;, и в которую могли бы включить конструкцию типа if. Я так понимаю в Rust не хотели чтобы if пришлось бы каждый раз глупо заканчивать ;, поэтому сделали исключение

Но это исключение распространяется не на все if expr, а только на те, которые возвращают unit type (()). Я думаю это логичное исключение, потому что unit type не может интерпритироваться как часть следующей инструкции, потому что над ним нельзя совершать операции. Так что если if expr возвращает unit type то это гарантированно отдельный statement

Оставшаяся часть if expr, которая возвращает всё остальное, например i32, всё ещё должна заканчиваться ;. То есть если if expr возвращает i32, то он должен заканчиваться ;, чтобы считаться отдельной инструкцией. Значит можно сделать вывод, что если не закончить if expr ;, то компилятор интерпретирует его как часть большей инструкции

Но на практике это совсем не так

Пример:

if true {
    1
} else {
    1
} + 1;

Здесь могла бы возникнуть неоднозначность в том, что if expr может быть как отдельная инструкция, а может быть как часть другого expr. Но нет, её тут нет, потому что if expr возвращает i32, а значит он может считаться полноценной инструкцией только с ; в конце. Получаеться никакой неоднозначности нет, if expr тут только как часть другого expr

Тем не менее, это не работает, работает только если обернуть в скобки весь if expression, убрав его с верхнего уровня

(if true {
    1
} else {
    1
}) + 1;

Ответы

▲ 1Принят

Это происходит из-за неоднозначности синтаксиса.

Вспомним грамматику Rust (здесь я заменил на многоточия несущественные сейчас детали):

BlockExpression = "{" … Statements "}"
Statements = Statement+ | Statement+ ExpressionWithoutBlock | ExpressionWithoutBlock

Statement = ";" | ExpressionStatement | …;
ExpressionStatement =  ExpressionWithoutBlock ";" | ExpressionWithBlock ";"?

ExpressionWithoutBlock = NegationExpression | ArithmeticOrLogicalExpression | …;
ExpressionWithBlock = IfExpression | …;

Блок состоит из утверждений (Statement, они же операторы), с опциональным выражением без блока (ExpressionWithoutBlock) в конце, утверждения могут быть утверждениями-выражениями (ExpressionStatement), последние бывают с блоками или без блоков.

Если утверждение-выражение без блока, то оно обязано завершаться точкой с запятой, но если оно содержит блок - то точка с запятой опциональна.

Наконец, выражение без блока может быть бинарной операцией (ArithmeticOrLogicalExpression) или унарной (NegationExpression). Последняя может быть только унарным "-" или "!", однако на всякий случай вариант с унарным "+" тоже проверяется (и запрещается).

Теперь попытаемся распарсить ваш первый пример:

if true {
    1
} else {
    1
} + 1;

Тут есть некоторая неоднозначность. Во-первых, это может быть одно утверждение:

Statement
  ExpressionStatement 
    ExpressionWithoutBlock
      ArithmeticOrLogicalExpression
        Expression "if true { 1 } else { 1 }"
        "+"
        Expression "1"
    ";" 

Во-вторых, это может быть два разных утверждения:

Statement
  ExpressionStatement 
    ExpressionWithBlock
      IfExpression "if true { 1 } else { 1 }"
Statement
  ExpressionStatement 
    ExpressionWithoutBlock
      NegationExpression
        "+"
        Expression "1"
    ";"

Для того, чтобы в языке было меньше "сюрпризов" как для пользователей, так и для авторов компилятора или IDE, авторы Rust ввели следующее правило: как только образуется законченное утверждение - этот вариант побеждает:

An expression that consists of only a block expression or control flow expression, if used in a context where a statement is permitted, can omit the trailing semicolon. This can cause an ambiguity between it being parsed as a standalone statement and as a part of another expression; in this case, it is parsed as a statement. -- https://doc.rust-lang.org/reference/statements.html#expression-statements

Именно потому компилятор понимает ваш код в соответствии с вариантом 2 и выдаёт ошибку.


Избавиться от этого можно если любым способом увести условный оператор из контекста блока в другой, где он никак не может оказаться Statement.

Можно добавить скобки вокруг него самого как в вашем втором примере:

(if true {
    1
} else {
    1
}) + 1

Можно добавить скобки вокруг всего выражения:

(if true {
    1
} else {
    1
} + 1)

Можно переставить операнды местами:

1 + if true {
    1
} else {
    1
}

Можно записать результат в переменную:

let x = if true {
    1
} else {
    1
} + 1;
x
▲ -1

На мой взгляд, если это не в скобках, то посылается в никуда. Как выражение верни 1. Куда? Никто не принимает. В случае, если будет присваивание в переменную или возврат значения из функции (без добавления), тогда все работает ок. С другой стороны обертка if-statement в {} лично у меня не работает, а обертка в () чего угодно у меня по-крайней мере не работает вообще (из мейна надо возвращать ()).

К сути: если возвращаешь значение в переменную - это if-expression. Если возвращаешь из функции - тоже. Если хочешь if-statement, не должно быть выражения +1, иначе это уже expression. Нужен контрпример, где скобки решают проблему!