Как избежать дублирования функций доступа для изменяемых и неизменных ссылок в Rust?

Несколько раз, я столкнулся с сценарием, где необходим метод accessor как для изменяемых, так и для неизменяемых ссылок.

Для ~ 3 строк не проблема дублировать логику, но когда логика становится более сложной, неплохо скопировать большие блоки кода.

Я хотел бы иметь возможность повторно использовать код для обоих.

Предоставляет ли Rust некоторый способ справиться с этим лучше, чем скопировать код или использовать unsafe casts?

например:.

impl MyStruct {
    pub fn get_foo(&self) -> &Bar {
        // ~20 lines of code
        // --- snip ---
        return bar;
    }
    pub fn get_foo_mut(&mut self) -> &mut Bar {
        // ~20 lines of code
        // (exactly matching previous code except `bar` is mutable)
        // --- snip ---
        return bar;
    }
}

Вот более подробная выдержка из базы кода, где неизменяемый возвращаемый аргумент был изменен на mutable для поддержки как неизменяемых, так и изменяемых версий функции. Это использует тип обернутого указателя (ConstP и MutP для неизменяемых и изменяемых ссылок), но логика функции должна быть ясной.

pub fn face_vert_share_loop<V, F>(f: F, v: V) -> LoopConstP
    where V: Into<VertConstP>,
          F: Into<FaceConstP>
{
    into_expand!(f, v);

    let l_first = f.l_first.as_const();
    let mut l_iter = l_first;
    loop {
        if l_iter.v == v {
            return l_iter;
        }

        l_iter = l_iter.next.as_const();
        if l_iter == l_first {
            break;
        }
    }

    return null_const();
}
pub fn face_vert_share_loop_mut(f: FaceMutP, v: VertMutP) -> LoopMutP {
    let l = face_vert_share_loop(f, v);
    return unsafe {
        // Evil! but what are the alternatives?
        // Perform an unsafe `const` to `mut` cast :(
        // While in general this should be avoided,
        // its 'OK' in this case since input is also mutable.
        l.as_mut()
    };
}

Ответы

Ответ 1

На самом деле вы этого не делаете. Напомним, что T, &T и &mut T - все разные типы. В этом контексте ваш вопрос совпадает с вопросом "Как избежать дублирования функций доступа для String и HashMap".

Matthieu M имел правильные термины "абстрактные над изменчивостью":

TL; DR - это то, что Rust, вероятно, нужно будет улучшить с помощью новых функций для поддержки этого. Поскольку никто не преуспел, никто не на 100% уверен, какие функции должны быть. В настоящее время лучше всего предположить, что более высокие типы типов (HKT).

Ответ 2

(ссылки на игровые площадки для решений с использованием параметры типа и связанные типы)

В этом случае &T и &mut T - всего два разных типа. Код, который является общим для разных типов (как во время компиляции, так и во время выполнения), идиоматически написан в Rust, используя черты. Например, данный:

struct Foo { value: i32 }
struct Bar { foo: Foo }

предположим, что мы хотим предоставить Bar общий аксессор для своего члена данных Foo. Аксессор должен работать как с &Bar, так и с &mut Bar, соответствующим образом возвращающим &Foo или &mut Foo. Поэтому мы пишем черту FooGetter

trait FooGetter {
    type Output;
    fn get(self) -> Self::Output;
}

задача которого должна быть общей для конкретного типа Bar. Его тип Output будет зависеть от Bar, так как мы хотим get иногда возвращать &Foo, а иногда &mut Foo. Обратите также внимание на то, что он потребляет self типа self. Поскольку мы хотим, чтобы get был общим для &Bar и &mut Bar, нам нужно реализовать FooGetter для обоих, так что self имеет соответствующие типы:

// FooGetter::Self == &Bar
impl<'a> FooGetter for &'a Bar {
    type Output = &'a Foo;
    fn get(self) -> Self::Output { & self.foo }
}

// FooGetter::Self == &mut Bar
impl<'a> FooGetter for &'a mut Bar {
    type Output = &'a mut Foo;
    fn get(mut self) -> Self::Output { &mut self.foo }
}

Теперь мы можем легко использовать .get() в общем коде, чтобы получить & или &mut ссылки на Foo из &Bar или &mut Bar (просто требуя T: FooGetter). Например:

// exemplary generic function:
fn foo<T: FooGetter>(t: T) -> <T as FooGetter>::Output {
    t.get() 
}

fn main() {
    let x = Bar { foo: Foo {value: 2} };
    let mut y = Bar { foo: Foo {value: 2} };

    foo(&mut y).value = 3;
    println!("{} {}\n", foo(&x).value, foo(&mut y).value);
}

Обратите внимание, что вы также можете реализовать FooGetter для Bar, так что get является общим по &T, &mut T и T сам (путем перемещения его). Это на самом деле метод .iter() реализован в стандартной библиотеке и почему он всегда делает "правильную вещь" независимо от ссылочного аргумента, на который он ссылается.

Ответ 3

В настоящее время Rust не поддерживает абстрагирование над изменчивостью.

Есть некоторые способы, которыми это может быть достигнуто, хотя они не идеальны:

  • Используйте макрос для разворачивания дубликата кода, объявления макроса и совместного использования между обеими функциями - необходимо сконструировать, чтобы он работал для изменяемого и неизменяемого, конечно.
  • Запишите неизменяемую версию функции (чтобы ничего не изменилось), затем напишите функцию-оболочку для изменяемой версии, которая выполняет преобразование unsafe на результат, чтобы сделать его изменяемым.

Ни один из них не очень привлекателен (макрос слишком многословный и немного менее читабельный, добавляет некоторый код-раздувание), unsafe более читабельна, но было бы неплохо избежать, поскольку приведение из неизменяемого в mutable isn 't так приятно иметь через базу кода.

На данный момент лучший вариант, насколько я могу видеть (где код для копирования не подходит), заключается в том, чтобы написать неизменяемую версию функции, а затем обернуть ее версией mut функции, в которой оба входы и выходы изменяемы.

Для этого требуется выражение unsafe на выходе функции, поэтому оно не идеально.


Примечание: важно, чтобы неизменяемая функция содержала тело кода, так как обратное позволит случайным образом изменить то, что может быть неизменным.