10 beneficios obvios de usar Rust

Rust es un lenguaje de programación de sistemas joven y ambicioso. Implementa la administración automática de memoria sin un recolector de basura y otros gastos generales de tiempo de ejecución. Además, el idioma predeterminado se usa en el idioma Rust, existen reglas sin precedentes para acceder a datos mutables y también se tienen en cuenta las vidas de los enlaces. Esto le permite garantizar la seguridad de la memoria y facilita la programación de subprocesos múltiples, debido a la falta de carreras de datos.



Todo esto ya es bien conocido por todos los que siguen el desarrollo de las tecnologías de programación modernas al menos un poco. Pero, ¿qué sucede si no es un programador de sistemas y no hay muchos códigos multiproceso en sus proyectos, pero todavía se siente atraído por el desempeño de Rust? ¿Obtendrá beneficios adicionales de su uso en aplicaciones? ¿O todo lo que él le dará además es una pelea difícil con el compilador, que lo obligará a escribir el programa para que siga constantemente las reglas del lenguaje sobre préstamos y propiedad?


Este artículo ha recopilado docenas de ventajas no obvias y no especialmente publicitadas del uso de Rust, que, espero, lo ayudará a decidir la elección de este idioma para sus proyectos.


1. La universalidad del lenguaje.


A pesar de que Rust se posiciona como un lenguaje para la programación del sistema, también es adecuado para resolver problemas aplicados de alto nivel. No tiene que trabajar con punteros sin procesar a menos que lo necesite para su tarea. La biblioteca de idiomas estándar ya ha implementado la mayoría de los tipos y funciones que pueden ser necesarios en el desarrollo de aplicaciones. También puede conectar fácilmente bibliotecas externas y usarlas. El sistema de tipos y la programación generalizada en Rust permiten el uso de abstracciones de un nivel bastante alto, aunque no hay soporte directo para OOP en el lenguaje.


Veamos algunos ejemplos simples del uso de Rust.


Un ejemplo de combinar dos iteradores en un iterador sobre pares de elementos:


let zipper: Vec<_> = (1..).zip("foo".chars()).collect(); assert_eq!((1, 'f'), zipper[0]); assert_eq!((2, 'o'), zipper[1]); assert_eq!((3, 'o'), zipper[2]); 

Correr


Nota: ¡una llamada al name!(...) del formato name!(...) es una llamada a una macro funcional. ¡Los nombres de tales macros en Rust siempre terminan con un símbolo ! para que puedan distinguirse de los nombres de funciones y otros identificadores. Los beneficios del uso de macros se discutirán a continuación.

Un ejemplo de uso de la biblioteca externa de expresiones regulares para trabajar con expresiones regulares:


 extern crate regex; use regex::Regex; let re = Regex::new(r"^\d{4}-\d{2}-\d{2}$").unwrap(); assert!(re.is_match("2018-12-06")); 

Correr


Un ejemplo de la implementación del Add para la propia estructura Point para sobrecargar el operador de suma:


 use std::ops::Add; struct Point { x: i32, y: i32, } impl Add for Point { type Output = Point; fn add(self, other: Point) -> Point { Point { x: self.x + other.x, y: self.y + other.y } } } let p1 = Point { x: 1, y: 0 }; let p2 = Point { x: 2, y: 3 }; let p3 = p1 + p2; 

Correr


Un ejemplo de uso de un tipo genérico en una estructura:


 struct Point<T> { x: T, y: T, } let int_origin = Point { x: 0, y: 0 }; let float_origin = Point { x: 0.0, y: 0.0 }; 

Correr


En Rust puede escribir utilidades de sistema eficientes, grandes aplicaciones de escritorio, microservicios, aplicaciones web (incluida la parte del cliente, ya que Rust puede compilarse en Wasm), aplicaciones móviles (aunque el ecosistema del lenguaje aún está poco desarrollado en esta dirección). Tal versatilidad puede ser una ventaja para los equipos de proyectos múltiples, ya que le permite utilizar los mismos enfoques y los mismos módulos en muchos proyectos diferentes. Si está acostumbrado al hecho de que cada herramienta está diseñada para su campo de aplicación limitado, intente ver a Rust como una caja de herramientas con la misma confiabilidad y conveniencia. Quizás esto es exactamente lo que te estabas perdiendo.


2. Herramientas convenientes de construcción y gestión de dependencias


Esto claramente no se anuncia, pero muchos notan que Rust tiene uno de los mejores sistemas de gestión de dependencia y compilación disponibles en la actualidad. Si programó en C o C ++, y la cuestión del uso indoloro de bibliotecas externas fue bastante aguda para usted, entonces usar Rust con su herramienta de compilación y el administrador de dependencia de Cargo será una buena opción para sus nuevos proyectos.


Además del hecho de que Cargo descargará las dependencias para usted y administrará sus versiones, creará y ejecutará sus aplicaciones, ejecutará pruebas y generará documentación, también se puede ampliar con complementos para otras funciones útiles. Por ejemplo, hay extensiones que le permiten a Cargo determinar las dependencias obsoletas de su proyecto, realizar análisis estáticos del código fuente, construir y volver a implementar partes del cliente de aplicaciones web y mucho más.


El archivo de configuración de Cargo utiliza el lenguaje de marcado toml amigable y mínimo para describir la configuración del proyecto. Aquí hay un ejemplo de un Cargo.toml configuración típico de Cargo.toml :


 [package] name = "some_app" version = "0.1.0" authors = ["Your Name <you@example.com>"] [dependencies] regex = "1.0" chrono = "0.4" [dev-dependencies] rand = "*" 

Y a continuación hay tres comandos típicos para usar Cargo:


 $ cargo check $ cargo test $ cargo run 

Con su ayuda, el código fuente se verificará en busca de errores de compilación, el ensamblaje del proyecto y el lanzamiento de las pruebas, el ensamblaje y el lanzamiento del programa para su ejecución, respectivamente.


3. Pruebas incorporadas


Escribir pruebas unitarias en Rust es tan fácil y simple que desea hacerlo una y otra vez. :) A menudo será más fácil escribir una prueba unitaria que intentar probar la funcionalidad de otra manera. Aquí hay un ejemplo de funciones y pruebas para ellos:


 pub fn is_false(a: bool) -> bool { !a } pub fn add_two(a: i32) -> i32 { a + 2 } #[cfg(test)] mod test { use super::*; #[test] fn is_false_works() { assert!(is_false(false)); assert!(!is_false(true)); } #[test] fn add_two_works() { assert_eq!(1, add_two(-1)); assert_eq!(2, add_two(0)); assert_eq!(4, add_two(2)); } } 

Correr


Las funciones en el módulo de test , marcadas con el atributo #[test] , son pruebas unitarias. Se ejecutarán en paralelo cuando se llame al comando de cargo test . El atributo de compilación condicional #[cfg(test)] , que marca todo el módulo con pruebas, conducirá al hecho de que el módulo se compilará solo cuando se ejecuten las pruebas, pero no entrará en el ensamblaje normal.


Es muy conveniente colocar las pruebas en el mismo módulo que el funcional bajo prueba, simplemente agregando el submódulo de test . Y si necesita pruebas de integración, simplemente coloque sus pruebas en el directorio de tests en la raíz del proyecto y use su aplicación en ellas como un paquete externo. No es necesario agregar un módulo de test separado y directivas de compilación condicional en este caso.


Ejemplos especiales de documentación ejecutada como pruebas merecen especial atención, pero esto se discutirá a continuación.


Las pruebas de rendimiento incorporadas (puntos de referencia) también están disponibles, pero aún no son estables, por lo tanto, solo están disponibles en ensamblajes nocturnos del compilador. En Rust estable, deberá utilizar bibliotecas externas para este tipo de pruebas.


4. Buena documentación con ejemplos actuales.


La biblioteca estándar de Rust está muy bien documentada. La documentación HTML se genera automáticamente a partir del código fuente con descripciones de rebajas en los comentarios del muelle. Además, los comentarios del documento en el código Rust contienen código de muestra que se ejecuta cuando se ejecutan las pruebas. Esto asegura la relevancia de los ejemplos:


 /// Returns a byte slice of this `String`'s contents. /// /// The inverse of this method is [`from_utf8`]. /// /// [`from_utf8`]: #method.from_utf8 /// /// # Examples /// /// Basic usage: /// /// ``` /// let s = String::from("hello"); /// /// assert_eq!(&[104, 101, 108, 108, 111], s.as_bytes()); /// ``` #[inline] #[stable(feature = "rust1", since = "1.0.0")] pub fn as_bytes(&self) -> &[u8] { &self.vec } 

La documentación


Aquí hay un ejemplo del uso del método as_bytes de tipo String


 let s = String::from("hello"); assert_eq!(&[104, 101, 108, 108, 111], s.as_bytes()); 

se ejecutará como prueba durante el lanzamiento de las pruebas.


Además, la práctica de crear ejemplos de su uso en forma de pequeños programas independientes ubicados en el directorio de examples en la raíz del proyecto es común para las bibliotecas Rust. Estos ejemplos también son una parte importante de la documentación y también se compilan y ejecutan durante la ejecución de la prueba, pero se pueden ejecutar independientemente de las pruebas.


5. Autodeducción inteligente de tipos


En un programa Rust, no puede especificar explícitamente el tipo de expresión si el compilador puede generarlo automáticamente en función del contexto de uso. Y esto se aplica no solo a aquellos lugares donde se declaran variables. Veamos un ejemplo:


 let mut vec = Vec::new(); let text = "Message"; vec.push(text); 

Correr


Si organizamos las anotaciones de tipo, este ejemplo se verá así:


 let mut vec: Vec<&str> = Vec::new(); let text: &str = "Message"; vec.push(text); 

Es decir, tenemos un vector de segmentos de cadena y una variable de tipo segmento de cadena. Pero en este caso, la especificación de tipos es completamente redundante, ya que el compilador puede generarlos por sí mismo (usando la versión extendida del algoritmo Hindley-Milner ). El hecho de que vec es un vector ya está claro por el tipo del valor de retorno de Vec::new() , pero aún no está claro qué tipo de elementos serán. El hecho de que el tipo de text sea ​​un segmento de cadena es comprensible por el hecho de que se le asigna un literal de este tipo. Por lo tanto, después de vec.push(text) , el tipo de elementos vectoriales se vuelve obvio. Tenga en cuenta que el tipo de la variable vec estaba completamente determinado por su uso en el hilo de ejecución, y no en la etapa de inicialización.


Tal sistema de inferencia de tipos elimina el ruido del código y lo hace tan conciso como el código en algún lenguaje de programación escrito dinámicamente. ¡Y esto mientras se mantiene una estricta escritura estática!


Por supuesto, no podemos deshacernos por completo de escribir en un idioma estáticamente escrito. El programa debe tener puntos en los que se garantice que se conocen los tipos de objetos, de modo que en otros lugares se puedan mostrar estos tipos. Dichos puntos en Rust son declaraciones de tipos de datos definidos por el usuario y firmas de funciones, en los que uno no puede sino especificar los tipos utilizados. Pero puede ingresar "metavariables de tipos" en ellos, utilizando la programación generalizada.


6. Coincidencia de patrones en puntos de declaración variables


let operación


 let p = Point::new(); 

en realidad no se limita a declarar nuevas variables. Lo que realmente hace es unir la expresión a la derecha del signo igual con el patrón de la izquierda. Y se pueden introducir nuevas variables como parte de la muestra (y solo así). Eche un vistazo al siguiente ejemplo y le resultará más claro:


 let Point { x, y } = Point::new(); 

Correr


La desestructuración se realizó aquí: dicha comparación introducirá las variables x e y , que se inicializarán con el valor de los campos x e y del objeto de la estructura Point , que se devuelve llamando a Point::new() . Al mismo tiempo, la comparación es correcta, ya que el tipo de expresión a la derecha corresponde al patrón de Point de tipo Point a la izquierda. De manera similar, puede tomar, por ejemplo, los dos primeros elementos de una matriz:


 let [a, b, _] = [1, 2, 3]; 

Y mucho más por hacer. Lo más notable es que tales comparaciones se realizan en todos los lugares donde se pueden ingresar nuevos nombres de variables en Rust, a saber: en el match , let , if let , while let if let , en el encabezado del bucle for , en los argumentos de funciones y cierres. Aquí hay un ejemplo del uso elegante de la coincidencia de patrones en un bucle for :


 for (i, ch) in "foo".chars().enumerate() { println!("Index: {}, char: {}", i, ch); } 

Correr


El método de enumerate , llamado en el iterador, construye un nuevo iterador, que iterará no sobre los valores iniciales, sino sobre tuplas, pares "índice ordinal, valor inicial". Cada una de estas tuplas durante la iteración del ciclo se correlacionará con el patrón especificado (i, ch) , como resultado de lo cual la variable i recibirá el primer valor de la tupla, el índice, y la variable ch , el segundo, es decir, el carácter de la cadena. Además en el cuerpo del bucle podemos usar estas variables.


Otro ejemplo popular de usar un patrón en un bucle for :


 for _ in 0..5 { //   5  } 

Aquí simplemente ignoramos el valor del iterador usando el patrón _ . Porque no usamos el número de iteración en el cuerpo del bucle. Lo mismo se puede hacer, por ejemplo, con un argumento de función:


 fn foo(a: i32, _: bool) { //      } 

O cuando coincide en una declaración de match :


 match p { Point { x: 1, .. } => println!("Point with x == 1 detected"), Point { y: 2, .. } => println!("Point with x != 1 and y == 2 detected"), _ => (), //        } 

Correr


La coincidencia de patrones hace que el código sea muy compacto y expresivo, y en la declaración de match generalmente es insustituible. El operador de match es un operador de análisis variativo completo, por lo que no podrá olvidarse accidentalmente de verificar algunas de las posibles coincidencias para la expresión analizada en él.


7. Extensión de sintaxis y DSL personalizado


La sintaxis de Rust es limitada, en gran parte debido a la complejidad del sistema de tipos utilizado en el lenguaje. Por ejemplo, Rust no tiene argumentos de funciones con nombre o funciones con un número variable de argumentos. Pero puede sortear estas y otras limitaciones con macros. Rust tiene dos tipos de macros: declarativa y procesal. Con las macros declarativas, nunca tendrá los mismos problemas que con las macros en C, porque son higiénicas y no funcionan a nivel de reemplazo de texto, sino a nivel de reemplazo en el árbol de sintaxis abstracta. Las macros le permiten crear abstracciones a nivel de sintaxis del lenguaje. Por ejemplo:


 println!("Hello, {name}! Do you know about {}?", 42, name = "User"); 

Además del hecho de que esta macro expande las capacidades sintácticas de llamar a la "función" de imprimir una cadena con formato, también en su implementación verificará que los argumentos de entrada coincidan con la cadena de formato especificada en tiempo de compilación y no en tiempo de ejecución. Usando macros, puede ingresar una sintaxis concisa para sus propias necesidades de diseño, crear y usar DSL. Aquí hay un ejemplo del uso de código JavaScript dentro de un programa Rust compilando en Wasm:


 let name = "Bob"; let result = js! { var msg = "Hello from JS, " + @{name} + "!"; console.log(msg); alert(msg); return 2 + 2; }; println!("2 + 2 = {:?}", result); 

Macro js! definido en el paquete stdweb y le permite incrustar código JavaScript completo en su programa (con la excepción de cadenas entre comillas simples y operadores no completados con punto y coma) y usar objetos del código Rust usando la sintaxis @{expr} .


Las macros ofrecen enormes oportunidades para adaptar la sintaxis de los programas de Rust a las tareas específicas de un área temática particular. Ahorrarán su tiempo y atención al desarrollar aplicaciones complejas. No aumentando la sobrecarga de tiempo de ejecución, sino aumentando el tiempo de compilación. :)


8. Autogeneración de código dependiente


Las macros de derivación procesal de Rust se usan ampliamente para implementar automáticamente rasgos y otra generación de código. Aquí hay un ejemplo:


 #[derive(Copy, Clone, Debug, Default, PartialEq, Eq)] struct Point { x: i32, y: i32, } 

Dado que todos estos tipos ( Copy , Clone , Debug , Default , PartialEq y Eq ) de la biblioteca estándar se implementan para el tipo de campos de la estructura i32 , su implementación se puede mostrar automáticamente para toda la estructura como un todo. Otro ejemplo:


 extern crate serde_derive; extern crate serde_json; use serde_derive::{Serialize, Deserialize}; #[derive(Serialize, Deserialize)] struct Point { x: i32, y: i32, } let point = Point { x: 1, y: 2 }; //  Point  JSON . let serialized = serde_json::to_string(&point).unwrap(); assert_eq!("{\"x\":1,\"y\":2}", serialized); //  JSON   Point. let deserialized: Point = serde_json::from_str(&serialized).unwrap(); 

Correr


Aquí, utilizando las Deserialize y Deserialize de la biblioteca de serde para la estructura Point , se generan automáticamente métodos para su serialización y deserialización. Luego puede pasar una instancia de esta estructura a varias funciones de serialización, por ejemplo, convirtiéndola en una cadena JSON.


Puede crear sus propias macros de procedimiento que generarán el código que necesita. O use las muchas macros ya creadas por otros desarrolladores. Además de salvar al programador de escribir código repetitivo, las macros también tienen la ventaja de que no necesita mantener diferentes secciones de código en un estado coherente. Por ejemplo, si se agrega un tercer campo z a la estructura de Point , entonces para hacer su serialización correctamente, si usa derivar, no necesita hacer nada más. Si nosotros mismos implementamos los rasgos necesarios para la serialización de Point , entonces tendremos que asegurarnos de que esta implementación siempre sea consistente con los últimos cambios en la estructura de Point .


9. Tipo de datos algebraicos


En pocas palabras, un tipo de datos algebraicos es un tipo de datos compuesto que es una unión de estructuras. Más formalmente, es una suma de tipos de tipos de productos. En Rust, este tipo se define usando la palabra clave enum :


 enum Message { Quit, ChangeColor(i32, i32, i32), Move { x: i32, y: i32 }, Write(String), } 

El tipo de un valor particular de una variable de tipo Message puede ser solo uno de los tipos de estructura enumerados en Message . Esta es una estructura Quit campo tipo unidad, una de las estructuras de tupla ChangeColor o Write con campos sin nombre, o la estructura Move habitual. Un tipo enumerado tradicional se puede representar como un caso especial de un tipo de datos algebraico:


 enum Color { Red, Green, Blue, White, Black, Unknown, } 

Es posible averiguar qué tipo realmente tomó valor en un caso particular utilizando la coincidencia de patrones:


 let color: Color = get_color(); let text = match color { Color::Red => "Red", Color::Green => "Green", Color::Blue => "Blue", _ => "Other color", }; println!("{}", text); ... fn process_message(msg: Message) { match msg { Message::Quit => quit(), Message::ChangeColor(r, g, b) => change_color(r, g, b), Message::Move { x, y } => move_cursor(x, y), Message::Write(s) => println!("{}", s), }; } 

Correr


En forma de tipos de datos algebraicos, Rust implementa tipos tan importantes como Option y Result , que se utilizan para representar el valor faltante y el resultado correcto / erróneo, respectivamente. Así es como se define la Option en la biblioteca estándar:


 pub enum Option<T> { None, Some(T), } 

Rust no tiene un valor nulo, al igual que los molestos errores de una llamada inesperada. En cambio, donde es realmente necesario indicar la posibilidad de un valor faltante, Option usa la Option :


 fn divide(numerator: f64, denominator: f64) -> Option<f64> { if denominator == 0.0 { None } else { Some(numerator / denominator) } } let result = divide(2.0, 3.0); match result { Some(x) => println!("Result: {}", x), None => println!("Cannot divide by 0"), } 

Correr


El tipo de datos algebraicos es una herramienta poderosa y expresiva que abre la puerta al desarrollo impulsado por tipos. Un programa escrito de manera competente en este paradigma asigna la mayoría de las comprobaciones de la corrección de su trabajo al sistema de tipos. Por lo tanto, si le falta un poco de Haskell en la programación industrial diaria, Rust puede ser su salida. :)


10. Refactorización fácil


El estricto sistema de tipo estático desarrollado en Rust y el intento de realizar tantas verificaciones como sea posible durante la compilación, lleva al hecho de que modificar y refactorizar el código se vuelve bastante simple y seguro. Si, después de los cambios, se compiló el programa, esto significa que solo dejó errores lógicos que no estaban relacionados con la funcionalidad cuya verificación se asignó al compilador. Combinado con la facilidad de agregar pruebas unitarias a la lógica de prueba, esto lleva a serias garantías de la confiabilidad de los programas y a un aumento en la confianza del programador en la operación correcta de su código después de realizar cambios.




Quizás esto es todo de lo que quería hablar en este artículo. Por supuesto, Rust tiene muchas otras ventajas, así como una serie de inconvenientes (cierta humedad del lenguaje, falta de modismos de programación familiares y sintaxis "no literaria"), que no se mencionan aquí. Si tiene algo que contar sobre ellos, escriba los comentarios. En general, prueba Rust en la práctica. Y tal vez sus ventajas para usted superen todas sus deficiencias, como sucedió en mi caso. Y finalmente, obtendrá exactamente el conjunto de herramientas que necesitaba durante mucho tiempo.

Source: https://habr.com/ru/post/es430294/


All Articles