Что делает mem::take в Write::write для среза &mut [u8]?

Исследуя, как перезаписать часть фрагмента байтов ([u8]), я заметил следующую реализацию Write::write в стандартной библиотеке Rust. (См. также исходники на GitHub.)

#[stable(feature = "rust1", since = "1.0.0")]
impl Write for &mut [u8] {
    #[inline]
    fn write(&mut self, data: &[u8]) -> io::Result<usize> {
        let amt = cmp::min(data.len(), self.len());
        let (a, b) = mem::take(self).split_at_mut(amt);
        a.copy_from_slice(&data[..amt]);
        *self = b;
        Ok(amt)
    }
    ...
}

Согласно документации, эта функция должна записывать заданный data в self. Однако я не понимаю, как работает эта функция.

let (a, b) = mem::take(self).split_at_mut(amt);

Насколько я понимаю, mem::take должен вернуть копию содержимого self. Он также должен заменить содержимое self пустым фрагментом, что бы это ни значило, но сейчас это не актуально. После этого эта копия разбивается на две части: a и b.

a.copy_from_slice(&data[..amt]);

Это единственная строка, где мы действительно используем данный data. Однако мы копируем его только в a, который сам по себе является лишь частью копии. После этого a больше никогда не используется. Как содержимое data попадает в self?

*self = b;

Я также не уверен, что делает этот последний оператор, но, похоже, он ничего не делает с нашими входящими данными. Если бы я проигнорировал документацию и имя функции, я бы интуитивно ожидал от реализации следующего результата:

fn main() {
    use std::io::Write;
    let mut buffer = String::from("abcdefg");
    let _ = unsafe { buffer.as_bytes_mut().write(b"xyz") };
    println!("{:?}", buffer);
    // Intuitive expectation: "defdefg"
    // Actual result:         "xyzdefg"
}

Фактический результат соответствует тому, что я ожидал на основе имени функции и документации, но не на основе реализации функции. Что мне не хватает?

Обратите внимание, что Self — это &mut [u8], поэтому получатель self имеет тип &mut &mut [u8] (двойная изменяемая ссылка).

cafce25 06.07.2024 09:30

@cafce25 После просмотра вашего комментария я думаю, что отчасти мое замешательство вызвано тем, что &mut [u8] — это не изменяемая ссылка на [u8], а другая конструкция, отличная от &mut перед другими типами. Другими словами, &mut [u8] — это весь кусочек, а не только [u8]. Я знал, что &mut перед [u8] делает изменяемым содержимое среза, а не сам срез, что, я думаю, привело меня к бессознательной предвзятости, согласно которой вы не можете иметь (изменяемые) ссылки на срез, что явно неверно. (Возможно, это также вызвало бы различные противоречия с остальными моими знаниями.)

JojOatXGME 06.07.2024 10:56

Это немного неправильно: [u8] — это срез, но обычно мы говорим срез, когда на самом деле имеем в виду ссылку на срез (& [u8]), потому что срезы нуждаются в косвенности. Итак, &mut [u8] на самом деле является изменяемой ссылкой на фрагмент байтов [u8].

cafce25 06.07.2024 20:36

@cafce25 Хорошо, это имеет смысл. Ссылки на срезы по-прежнему отличаются от «обычных» ссылок. И смысл среза закодирован в этих специальных ссылках. В любом случае, спасибо, что указали на неправильную номенклатуру. То, что срезы являются не ссылками, а областью памяти, на которую указывают, имеет абсолютный смысл. Если посмотреть на это с этой точки зрения, синтаксис также станет более понятным. :D

JojOatXGME 08.07.2024 02:41
Почему Python в конце концов умрет
Почему Python в конце концов умрет
Последние 20 лет были действительно хорошими для Python. Он прошел путь от "просто языка сценариев" до основного языка, используемого для написания...
2
4
64
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Важно помнить, что срез в Rust не содержит никаких данных, он ссылается на данные. Срез — это просто указатель на начало некоторых данных и длину этих данных.

Во-вторых, метод write для срезов также «обновляет срез, чтобы он указывал на еще не написанную часть»:

fn main() {
    use std::io::Write;
    let mut buffer = String::from("abcdefg");
    let mut bytes = unsafe { buffer.as_bytes_mut() };
    let _ = bytes.write(b"xyz");
    println!("{:?}", bytes); // b"defg"
}

Вот что делает *self = b. b — это незаписанная часть среза, и мы присваиваем ее *self. Для этого нам нужно mem::take:

Без исключения времени жизни метод выглядит так:

impl<'slice> Write for &'slice mut [u8] {
    #[inline]
    fn write<'this>(&'this mut self, data: &[u8]) -> io::Result<usize> {
        let amt = cmp::min(data.len(), self.len());
        let (a, b) = mem::take(self).split_at_mut(amt);
        a.copy_from_slice(&data[..amt]);
        *self = b;
        Ok(amt)
    }
    ...
}

Таким образом, тип Self равен &'slice mut [u8] (потому что это тема блока impl), что означает, что тип &'this mut Self (т. е. тип self) равен &'this mut &'slice mut [u8]. Это означает, что для присвоения *self нам нужны данные, которые переживут 'slice, внешнее время жизни. Но поскольку self стоит за ссылкой, которая переживёт только 'this, мы можем получить из неё только разыменование на данные, которые переживут 'this. А поскольку split_at_mut имеет подпись split_at_mut<'a>(&'a mut self) -> (&'a mut T, &'a mut T), он даст нам только фрагменты, которые сохраняются 'this при вызове self (поскольку время жизни двух подсрезов привязано к времени жизни входных данных).

Нам нужно извлечь &'slice mut [u8] из &'this mut &'slice mut [u8], и именно это и делает mem::take. Он имеет (упрощенную) подпись take<T>(&mut T) -> T, в нашем случае take<&'slice mut [u8]>(&mut &'slice mut [u8]) -> &'slice mut [u8]. Он делает это, оставляя вместо него замену по умолчанию (в данном случае пустой фрагмент, который представляет собой просто указатель, который может или не может висеть, и длину 0).

Теперь a и b являются справочными данными, которые сохраняются после 'slice, и мы можем свободно присваивать b*self.

Правильно ли я вас понимаю, что mem::take фактически копирует только ссылку (т. е. фрагмент), но не контент? И в то же время, *self = b тоже только перезаписывает фрагмент? Я знаю, что срезы являются ссылками, но моя ошибка заключалась в том, что я не осознавал и не учитывал, что у вас может быть (изменяемая) ссылка на срез (которая сама по себе является ссылкой). Но, конечно, можно, спасибо за ответ.

JojOatXGME 06.07.2024 10:18

По сути, да. Байты, на которые ссылаются срезы, не перемещаются ни на каком этапе всей этой процедуры. take просто перемещает фрагмент из изменяемой ссылки, за которой он находится.

isaactfa 06.07.2024 10:53

Другие вопросы по теме