Я столкнулся с этим странным поведением при чтении этого сообщение, и основной вопрос этого поста — при сопоставлении (&k, &v) = &(&String, &String)
, k
и v
будет получен тип String
.
Чтобы понять, что происходит, я написал следующий тестовый код, и результат оказался для меня гораздо более шокирующим и запутанным:
fn main() {
let x: &(&String, &String) = &(&String::new(), &String::new());
let ref_to_x: &&(&String, &String) = &x;
let ref_ref_to_x: &&&(&String, &String) = &&x;
let ref_ref_ref_to_x: &&&&(&String, &String) = &&&x;
// code snippet 1
let (a, b) = x; // type of a: &&String, type of b: &&String
let (a, b) = ref_to_x; // type of a: &&String, type of b: &&String
let (a, b) = ref_ref_to_x; // type of a: &&String, type of b: &&String
let (a, b) = ref_ref_ref_to_x; // type of a: &&String, type of b: &&String
// code snippet 2
let &(a, b) = x; // type of a: &String, type of b: &String
let &(a, b) = ref_to_x; // type of a: &&String, type of b: &&String
let &(a, b) = ref_ref_to_x; // type of a: &&String, type of b: &&String
let &(a, b) = ref_ref_ref_to_x;// type of a: &&String, type of b: &&String
// code snippet 3
let (&a, &b) = x; // type of a: String, type of b: String
let (&a, &b) = ref_to_x; // type of a: String, type of b: String
let (&a, &b) = ref_ref_to_x; // type of a: String, type of b: String
let (&a, &b) = ref_ref_ref_to_x;// type of a: String, type of b: String
}
Аннотации типа a
и b
, которые я добавил в конец строки, обозначаются rust-analyzer
.
Пожалуйста, ПРИМЕЧАНИЕ, code snippet 3
не будет компилироваться из-за ошибки can not move out of xx because it's borrowrd/can not move out of xx which is behind a shared reference
, но я думаю, что это не имеет значения (Возможно, я ошибаюсь, если это так, укажите мне, спасибо), потому что мы концентрируемся на типе a
и b
.
Мои вопросы:
a
и b
всегда имеют один и тот же тип, даже если RHS имеют разные типы (x/ref_to_x/ref_ref_to_x..) во фрагменте кода 1/2/3?rust-analyzer/rustc
, при написании кода?Кстати, это относится к rfc 2005 match-эргономика? Я много гуглил и нашел, что многие люди упомянули об этом в своем ответе.
Это называется «деструктуризация». Он обычно используется при сопоставлении с образцом, например, с if let Some(val) = option
.
По сути, это:
let x: (&String, &String) = (&String::new(), &String::new());
let (&a, &b) = x; // a: String, b: String
Эквивалентно этому:
let (&a, &b) = (&String::new(), &String::new()); // a: String, b: String
Что эквивалентно этому:
let (a, b) = (String::new(), String::new());
Чтобы объяснить больше, я буду использовать более простой пример.
let value: String = String::new();
let &also_value = &value; // also_value: String
Деструктуризация работает как бы «упрощая» обе стороны. В этом случае будут разыменованы обе стороны, что приведет к
let also_value = value;
Это также работает через косвенность. Если мы добавим промежуточный шаг:
let value: String = String::new();
let ref_to_value: &String = &value;
let &also_value = ref_to_value; // also_value: String
Он может видеть через косвенность и теперь будет разыменовывать ref_to_value
, чтобы снова получить String
.
Эргономика сопоставления также может быть задействована, но только для разыменования всего кортежа.
Примечание: чтобы получить ссылку при назначении деструктурирования, вы можете вместо этого использовать ref
:
let value: String = String::new();
let ref ref_to_value = value; // ref_to_value: &String
Обычно это используется только при сопоставлении с образцом, например:
if let Some(ref x) = option { ... }
Да. То, что вы видите, соответствует эргономике в действии, и их поведение может быть не таким, как вы ожидаете.
Эргономика соответствия работает с использованием режимы привязки. Доступны три режима вязки, и их можно использовать даже без спичечной эргономики:
ref
. Это то, что вы получаете, когда применяете оператор ref
к привязке (что удивительно), и он добавляет ссылку один. Например, в match e { ref r => ... }
r
есть &e
.ref mut
. Аналогичен ref
, но использует изменяемое заимствование (и указывается с помощью оператора ref mut
).Процесс работает следующим образом: компилятор обрабатывает шаблон снаружи внутрь. Процесс начинается с move
в качестве режима привязки.
Каждый раз, когда компилятору необходимо сопоставить нереференсный образец (литерал, структуру, кортеж, срез) со ссылкой, он автоматически разыменовывает ссылку и обновляет режим привязки: когда ссылка &
сопоставляется, мы получаем режим привязки ref
, а для &mut
ссылки мы получим ref
если текущий режим привязки ref
или иначе ref mut
. Затем этот процесс повторяется до тех пор, пока у нас больше не будет ссылки.
Если мы сопоставляемся с эталонный образец (привязка, подстановочный знак, const
s ссылочных типов или шаблоны &
/&mut
), режим привязки по умолчанию сбрасывается обратно на move
.
Когда переменная привязывается, компилятор смотрит на текущий режим привязки: для move
он будет соответствовать типу как есть. Для ref
и ref mut
будет добавлено &
или &mut
соответственно. Но только один.
Давайте следовать вашим примерам построчно.
let (a, b) = x; // type of a: &&String, type of b: &&String
Мы сопоставляем нереференсный шаблон (шаблон кортежа) со ссылкой (типа &(&String, &String)
). Поэтому мы разыменовываем ссылку и устанавливаем режим привязки ref
.
Теперь у нас есть шаблон кортежа для сопоставления с кортежем типа (&String, &String)
и режимом привязки ref
. Мы сопоставляем a
с &String
: это эталонный шаблон (привязка), поэтому мы не меняем режим привязки. Однако у нас уже есть режим привязки ref
. Мы сопоставляем тип &String
, а ref
означает, что мы добавляем ссылку, поэтому мы заканчиваем на &&String
. Точно то же самое происходит с b
.
let (a, b) = ref_to_x; // type of a: &&String, type of b: &&String
Здесь, как и в предыдущем примере, мы сопоставляем нереференсный шаблон (шаблон кортежа) со ссылкой (&&(&String, &String)
). Итак, мы разыменовываем и устанавливаем режим привязки ref
. Но у нас все еще есть ссылка: &(&String, &String)
. Итак, мы снова разыменовываем. Режим привязки уже есть ref
, поэтому трогать его не нужно. Мы заканчиваем сопоставлением (a, b)
с (&String, &String)
. Это означает a = &String
, b = &String
. Но помните, что мы используем режим привязки ref
, поэтому мы должны добавить ссылку. Добавляем ссылку единственный, даже несмотря на то, что мы сошлись против двух! В конце имеем a = &&String
, b = &&String
.
Остальные примеры в этом фрагменте кода работают так же.
let &(a, b) = ref_to_x; // type of a: &&String, type of b: &&String
Здесь мы сначала сопоставляем шаблон &
со ссылкой типа &&(&String, &String)
. Это удаляет обе ссылки, заставляя нас сопоставлять (a, b)
с &(&String, &String)
. С этого момента мы продолжаем так же, как в первом примере.
Остальные примеры в этом фрагменте аналогичны.
let (&a, &b) = x; // type of a: String, type of b: String
Это самое интересное. Помните, как мы говорили об эталонных и нереференсных шаблонах? В данном примере этот факт играет решающую роль.
Сначала мы сопоставляем шаблон кортежа с типом &(&String, &String)
. Мы разыменовываем кортеж и устанавливаем binding_mode = ref
. Теперь мы сопоставляем кортеж: мы должны сопоставить &a
и &b
каждый с &String
, с установленным режимом привязки ref
.
Что произойдет, если мы сопоставим &a
с &String
? Ну, помните, что &
— это эталонный шаблон, и при сопоставлении эталонных шаблонов мы полностью игнорировать режим привязки. Итак, мы сопоставляем &a
с &String
, сбрасывая режим привязки на move
. Это удаляет ссылку с обеих сторон, оставляя нам a = String
. То же самое для &b
.
Следующие примеры в этом фрагменте кода такие же.
Спасибо за Ваш ответ! Но кажется, что этот ответ не отвечает на мой вопрос, пример, который вы привели, довольно прост. Что меня озадачило, так это то, почему
a
иb
имеют одинаковый тип во фрагменте кода 1/2/3, даже если RHS имеет разные типы (я обновлю свой вопрос, чтобы сделать его более ясным, извините за это)