Apéndice C: Traits derivables

En varios lugares del libro, hemos discutido el atributo derive, que puede aplicar a una definición de estructura o enumeración. El atributo derive genera código que implementará un rasgo con su propia implementación predeterminada en el tipo que ha anotado con la sintaxis derive.

En este apéndice, proporcionamos una referencia de todos los traits en la biblioteca estándar que puede usar con derive. Cada sección cubre:

  • Qué operadores y métodos que derivan este trait se habilitarán
  • Qué hace la implementación del trait proporcionado por derive
  • Qué significa implementar el trait sobre el tipo
  • Las condiciones en las que se le permite o no implementar el trait
  • Ejemplos de operaciones que requieren el trait

Si desea un comportamiento diferente al proporcionado por el atributo derive, consulte la documentación de la biblioteca estándar para cada trait para obtener detalles sobre cómo implementarlos manualmente.

Estos traits enumerados aquí son los únicos definidos por la biblioteca estándar que se pueden implementar en sus tipos usando derive. Otros traits definidos en la biblioteca estándar no tienen un comportamiento predeterminado sensato, por lo que depende de usted implementarlos de la manera que tenga sentido para lo que está tratando de lograr.

Un ejemplo de un trait que no se puede derivar es Display, que maneja el formateo para los usuarios finales. Siempre debe considerar la forma apropiada de mostrar un tipo a un usuario final. ¿Qué partes del tipo puede ver un usuario final? ¿Qué partes encontrarían relevantes? ¿Qué formato de los datos sería más relevante para ellos? El compilador Rust no tiene esta idea, por lo que no puede proporcionar un comportamiento predeterminado apropiado para usted.

La lista de traits derivables proporcionada en este apéndice no es exhaustiva: las bibliotecas pueden implementar derive para sus propios traits, lo que hace que la lista de traits que puede usar derive sea realmente abierta. Implementar derive implica usar una macro procedural, que se cubre en la sección “Macros” del Capítulo 19.

Debug para el Output del programador

El trait Debug permite el formateo de depuración en cadenas de formato, que indica agregando :? dentro de los marcadores {}.

El trait Debug te permite imprimir instancias de un tipo con fines de depuración, para que tú y otros programadores que usen tu tipo puedan inspeccionar una instancia en un punto particular de la ejecución de un programa.

El trait Debug es necesario, por ejemplo, en el uso de la macro assert_eq!. Esta macro imprime los valores de las instancias dadas como argumentos si la aserción de igualdad falla, por lo que los programadores pueden ver por qué las dos instancias no eran iguales.

PartialEq y Eq para comparaciones de igualdad

El trait PartialEq te permite comparar instancias de un tipo para verificar la igualdad y habilita el uso de los operadores == y !=.

Derivar PartialEq implementa el método eq. Cuando se deriva PartialEq en estructuras, dos instancias son iguales solo si todos los campos son iguales, y las instancias no son iguales si alguno de los campos no es igual. Cuando se deriva en enumeraciones, cada variante es igual a sí misma y no igual a las otras variantes.

El trait PartialEq es necesario, por ejemplo, con el uso de la macro assert_eq!, que necesita poder comparar dos instancias de un tipo para la igualdad.

El trait Eq no tiene métodos. Su propósito es señalar que para cada valor del tipo anotado, el valor es igual a sí mismo. El trait Eq solo se puede aplicar a tipos que también implementan PartialEq, aunque no todos los tipos que implementan PartialEq pueden implementar Eq. Un ejemplo de esto son los tipos de números de punto flotante: la implementación de los números de punto flotante establece que dos instancias del valor no es un número (NaN) no son iguales entre sí.

Un ejemplo de cuando se necesita Eq es para las claves en un HashMap<K, V> para que el HashMap<K, V> pueda decir si dos claves son iguales.

PartialOrd y Ord para comparaciones de orden

El trait PartialOrd te permite comparar instancias de un tipo para fines de ordenación. Un tipo que implementa PartialOrd se puede usar con los operadores <, >, <= y >=. Solo puede aplicar el trait PartialOrd a tipos que también implementan PartialEq.

Derivar PartialOrd implementa el método partial_cmp, que devuelve un Option<Ordering> que será None cuando los valores dados no produzcan un orden. Un ejemplo de un valor que no produce un orden, a pesar de que la mayoría de los valores de ese tipo se pueden comparar, es el valor de punto flotante no es un número (NaN). Llamar a partial_cmp con cualquier número de punto flotante y el valor de punto flotante NaN devolverá None.

Cuando se deriva en structs, PartialOrd compara dos instancias comparando el valor en cada campo en el orden en que aparecen los campos en la definición del struct. Cuando se deriva en enums, las variantes del enum declaradas anteriormente en la definición de la enumeración se consideran menores que las variantes enumeradas más tarde.

El trait PartialOrd es necesario, por ejemplo, para el método gen_range del crate rand que genera un valor aleatorio en el rango especificado por una expresión de rango.

El trait Ord permite saber que para cualquier dos valores del tipo anotado, existirá un orden válido. El trait Ord implementa el método cmp, que devuelve un Ordering en lugar de un Option<Ordering> porque siempre será posible un orden válido. Solo puede aplicar el trait Ord a tipos que también implementan PartialOrd y Eq (y Eq requiere PartialEq). Cuando se deriva en structs y enums, cmp se comporta de la misma manera que la implementación derivada para partial_cmp con PartialOrd.

Un ejemplo de cuando se necesita Ord es cuando se almacenan valores en un BTreeSet<T>, una estructura de datos que almacena datos basados en el orden de clasificación de los valores.

Clone y Copy para duplicar valores

El trait Clone te permite crear explícitamente una copia profunda de un valor, y el proceso de duplicación puede implicar la ejecución de código arbitrario y la copia de datos de la pila. Consulte la sección “Ways Variables and Data Interact: Clone” en el Capítulo 4 para obtener más información sobre Clone.

Derivar Clone implementa el método clone, que cuando se implementa para todo el tipo, llama a clone en cada una de las partes del tipo. Esto significa que todos los campos o valores en el tipo también deben implementar Clone para derivar Clone.

Un ejemplo de cuando se requiere Clone es cuando se llama al método to_vec en una rebanada. La rebanada no posee las instancias de tipo que contiene, pero el vector devuelto de to_vec necesitará poseer sus instancias, por lo que to_vec llama a clone en cada elemento. Por lo tanto, el tipo almacenado en la rebanada debe implementar Clone.

El trait Copy te permite duplicar un valor copiando solo los bits almacenados en la pila; no es necesario ningún código arbitrario. Consulte la sección “Stack-Only Data: Copy” en el Capítulo 4 para obtener más información sobre Copy.

El trait Copy no define ningún método para evitar que los programadores sobrecarguen esos métodos y violen la suposición de que no se está ejecutando código arbitrario. De esa manera, todos los programadores pueden asumir que copiar un valor será muy rápido.

Puede derivar Copy en un tipo solo si todas las partes del tipo implementan Copy. Un tipo que implementa Copy también debe implementar Clone, porque un tipo que implementa Copy tiene una implementación trivial de Clone que realiza la misma tarea que Copy.

El trait Copy es rara vez requerido; los tipos que implementan Copy tienen optimizaciones disponibles, lo que significa que no tiene que llamar a clone, lo que hace que el código sea más conciso.

Todo lo posible con Copy también se puede lograr con Clone, pero el código podría ser más lento o tener que usar clone en lugares.

Hash para mapear un valor a un valor de tamaño fijo

El trait Hash te permite tomar una instancia de un tipo de tamaño arbitrario y asignar esa instancia a un valor de tamaño fijo usando una función hash. Derivar Hash implementa el método hash. La implementación derivada del método hash combina el resultado de llamar a hash en cada una de las partes del tipo, lo que significa que todos los campos o valores también deben implementar Hash para derivar Hash.

Un ejemplo de cuando se requiere Hash es en el almacenamiento de claves en un HashMap<K, V> para almacenar datos de manera eficiente.

Default para valores predeterminados

El trait Default te permite crear un valor predeterminado para un tipo. Derivar Default implementa la función default. La implementación derivada de la función default llama a la función default en cada parte del tipo, lo que significa que todos los campos o valores en el tipo también deben implementar Default para derivar Default.

La función Default::default es comúnmente usada en combinación con la sintaxis de actualización de struct discutida en la sección “Creating Instances From Other Instances With Struct Update Syntax” en el Capítulo 5. Puede personalizar algunos campos de un struct y luego establecer y usar un valor predeterminado para el resto de los campos usando ..Default::default().

El trait Default es necesario, por ejemplo, cuando se usa el método unwrap_or_default en instancias de Option<T>. Si el Option<T> es None, el método unwrap_or_default devolverá el resultado de Default::default para el tipo T almacenado en el Option<T>.