¿Deberían duplicarse los límites de los rasgos en struct e impl?

Resuelto user2011659 asked hace 6 años • 3 respuestas

El siguiente código utiliza una estructura de tipo genérico. Si bien su implementación solo es válida para el límite de rasgo dado, la estructura se puede definir con o sin el mismo límite. Los campos de la estructura son privados, por lo que ningún otro código podría crear una instancia de todos modos.

trait Trait {
    fn foo(&self);
}

struct Object<T: Trait> {
    value: T,
}

impl<T: Trait> Object<T> {
    fn bar(object: Object<T>) {
        object.value.foo();
    }
}

¿Debería omitirse el rasgo vinculado a la estructura para cumplir con el principio DRY, o debería darse para aclarar la dependencia? ¿O hay circunstancias en las que se debería preferir una solución a la otra?

user2011659 avatar Mar 12 '18 13:03 user2011659
Aceptado

Creo que las respuestas existentes son engañosas. En la mayoría de los casos, no se debe poner un límite a una estructura a menos que la estructura literalmente no se pueda compilar. sin él.

tl; dr

Los límites de las estructuras expresan algo incorrecto para la mayoría de las personas. Son contagiosos, redundantes, a veces miopes y a menudo confusos. Incluso cuando te sientes bien con un salto, normalmente debes dejarlo hasta que se demuestre que es necesario.

(En esta respuesta, todo lo que digo sobre estructuras se aplica igualmente a las enumeraciones).


0. Los límites de las estructuras deben repetirse en todos los lugares donde se encuentre la estructura.

Esta es la razón más obvia, pero (para mí) menos convincente, para evitar escribir límites en estructuras. Al momento de escribir este artículo (Rust 1.65), debe repetir los límites de cada estructura en todo impllo que la toque, lo cual es una razón suficiente para no poner límites a las estructuras por ahora. Sin embargo, existe un RFC aceptado (implied_bounds ) aceptado que, cuando se implemente y se estabilice, cambiará esto al inferir los límites redundantes. Pero incluso entonces, los límites de las estructuras suelen ser incorrectos:

1. Los límites de las estructuras se escapan de las abstracciones.

Su estructura de datos es especial. " Object<T>Sólo tiene sentido si Tlo es Trait", dices. Y quizás tengas razón. Pero la decisión afecta no sólo a Object, sino a cualquier otra estructura de datos que contenga un Object<T>, incluso si no siempre contiene un Object<T>. Considere un programador que quiere envolverlo Objecten un enum:

enum MyThing<T> {  // error[E0277]: the trait bound `T: Trait` is not satisfied
    Wrapped(your::Object<T>),
    Plain(T),
}

Dentro del código posterior, esto tiene sentido porque MyThing::Wrappedsolo se usa con Tmensajes de correo electrónico que implementan Thing, mientras que Plainse puede usar con cualquier tipo. Pero si your::Object<T>tiene un límite T, enumno se puede compilar sin ese mismo límite, incluso si hay muchos usos para a Plain(T)que no requieren dicho límite. Esto no solo no funciona, sino que incluso si agregar el límite no lo hace completamente inútil, también expone el límite en la API pública de cualquier estructura que useMyThing .

Los límites de las estructuras limitan lo que otras personas pueden hacer con ellas. Los límites en el código ( implsy funciones ) también lo hacen, por supuesto, pero esas restricciones (presumiblemente) son requeridas por su propio código, mientras que los límites en las estructuras son un ataque preventivo contra cualquiera que pueda usar su estructura de una manera innovadora. Esto puede ser útil, pero los límites innecesarios son particularmente molestos para los innovadores porque restringen lo que se puede compilar sin restringir de manera útil lo que realmente se puede ejecutar. (más sobre esto en un momento).

2. Los límites de las estructuras son redundantes con los límites del código.

¿Entonces no cree que la innovación downstream sea posible? Eso no significa que la estructura en sí necesite un límite. Para que sea imposible construir un Object<T>sin T: Trait, basta con poner ese límite en el constructor que implcontiene ; Si es imposible invocar un sin, puedes decirlo en el que contiene , o tal vez en sí mismo. (HastaObjecta_methodObject<T>T: Traitimpla_methoda_methodimplied_bounds se implemente, debe hacerlo de todos modos, por lo que ni siquiera tiene la débil justificación de "guardar pulsaciones de teclas").

Incluso y especialmente cuando no se le ocurre ninguna forma de que downstream utilice un unbounded Object<T>, no debe prohibirlo a priori , porque...

3. Los límites de las estructuras significan algo diferente para el sistema de tipos que los límites del código.

Un T: Traitlímite Object<T>significa más que "todos Object<T>tenemos que tener T: Trait"; en realidad significa algo así como "el concepto de Object<T>sí mismo no tiene sentido a menos que T: Trait", que es una idea más abstracta. Piense en el lenguaje natural: nunca he visto un elefante morado, pero puedo nombrar fácilmente el concepto de "elefante morado" a pesar de que no corresponde a ningún animal del mundo real. Los tipos son un tipo de lenguaje y puede tener sentido hacer referencia a la idea de Elephant<Purple>, incluso cuando no sabes cómo crear uno y ciertamente no te sirve. De manera similar, puede tener sentido expresar el tipo Object<NotTrait>en abstracto incluso si no tienes ni puedes tener uno a mano en este momento. Especialmente cuando NotTraites un parámetro de tipo, cuya implementación puede no ser conocida en este contextoTrait , pero en algún otro contexto sí.

Caso de estudio:Cell<T>

Para ver un ejemplo de una estructura que originalmente tenía un límite de rasgo que finalmente se eliminó, no busque más allá de Cell<T>, que originalmente tenía un T: Copylímite. En el RFC para eliminar el límite, muchas personas inicialmente presentaron el mismo tipo de argumentos en los que quizás estés pensando en este momento, pero el consenso final fue que " Cellrequiere Copy" siempre fue la forma incorrecta de pensar Cell. El RFC se fusionó, allanando el camino para innovaciones como Cell::as_slice_of_cells, que le permite hacer cosas que antes no podía hacer en un código seguro, incluida la suscripción temporal a la mutación compartida . El punto es que T: Copynunca fue un límite útil Cell<T>, y no habría hecho ningún daño (y posiblemente algo bueno) dejarlo fuera desde el principio.

Este tipo de restricción abstracta puede ser difícil de entender, lo que probablemente sea una de las razones por las que a menudo se usa mal. Lo que se relaciona con mi último punto:

4. Los límites innecesarios invitan a parámetros innecesarios (que son peores).

Esto no se aplica a todos los casos de límites de estructuras, pero es un punto común de confusión. Es posible, por ejemplo, tener una estructura con un parámetro de tipo que debería implementar un rasgo genérico, pero no saber qué parámetros debe tomar el rasgo. En tales casos, es tentador usarlo PhantomDatapara agregar un parámetro de tipo a la estructura principal, pero esto suele ser un error, sobre todo porque PhantomDataes difícil de usar correctamente. A continuación se muestran algunos ejemplos de parámetros innecesarios agregados debido a límites innecesarios: 1 2 3 4 5 En la mayoría de estos casos, la solución correcta es simplemente eliminar el límite.

Excepciones a la regla

Bien, ¿cuándo necesitas un límite en una estructura? Puedo pensar en dos posibles razones.

  • En la respuesta de Shepmaster , la estructura simplemente no se compilará sin un límite, porque la Iteratorimplementación Idefine realmente lo que contiene la estructura. Otra forma en que una estructura no se compilará sin un límite es cuando su implementación Droptiene que usar el rasgo de alguna manera. DropNo se pueden tener límites que no estén en la estructura, por razones de solidez, por lo que también debes escribirlos en la estructura.

  • Cuando estás escribiendo unsafecódigo y quieres que dependa de un límite ( T: Send, por ejemplo), es posible que tengas que colocar ese límite en la estructura. unsafeEl código es especial porque puede depender de invariantes que están garantizadas por no unsafecódigo, por lo que simplemente poner el límite en el implque contiene unsafeno es necesariamente suficiente.

Pero en todos los demás casos, a menos que realmente sepa lo que está haciendo, debe evitar por completo los límites de las estructuras.

trent avatar Feb 25 '2021 13:02 trent

Los límites de rasgos que se aplican a cada instancia de la estructura deben aplicarse a la estructura:

struct IteratorThing<I>
where
    I: Iterator,
{
    a: I,
    b: Option<I::Item>,
}

Los límites de rasgos que solo se aplican a ciertas instancias solo deben aplicarse al implbloque al que pertenecen:

struct Pair<T> {
    a: T,
    b: T,
}

impl<T> Pair<T>
where
    T: std::ops::Add<T, Output = T>,
{
    fn sum(self) -> T {
        self.a + self.b
    }
}

impl<T> Pair<T>
where
    T: std::ops::Mul<T, Output = T>,
{
    fn product(self) -> T {
        self.a * self.b
    }
}

para cumplir con el principio DRY

La redundancia será eliminada por RFC 2089 :

Eliminar la necesidad de límites "redundantes" en funciones e implicaciones donde esos límites se pueden inferir de los tipos de entrada y otros límites de rasgos. Por ejemplo, en este programa simple, la impl ya no requeriría un límite, porque se puede inferir del Foo<T>tipo:

struct Foo<T: Debug> { .. }
impl<T: Debug> Foo<T> {
  //    ^^^^^ this bound is redundant
  ...
}
Shepmaster avatar Mar 12 '2018 17:03 Shepmaster