Primeros pasos para el óxido

imagen


Hola a todos Recientemente me encontré con un nuevo lenguaje de programación Rust. Me di cuenta de que era diferente de los demás que había encontrado antes. Por lo tanto, decidí cavar más profundo. Quiero compartir los resultados y mis impresiones:


  • Comenzaré con las características principales, en mi opinión, de Rust
  • Describiré detalles de sintaxis interesantes
  • Explicaré por qué no es probable que Rust se apodere del mundo

Explicaré de inmediato que he estado escribiendo en Java durante unos diez años, así que discutiré desde mi campanario.


Característica asesina


Rust está tratando de tomar una posición intermedia entre los lenguajes de bajo nivel como C / C ++ y Java / C # / Python / Ruby de alto nivel ... Cuanto más cerca esté el lenguaje del hardware, más control, más fácil será predecir cómo se ejecutará el código. Pero tener acceso completo a la memoria es mucho más fácil de dispararle a la pierna. A diferencia de C / C ++, apareció Python / Java y todo lo demás. No necesitan pensar en borrar la memoria. Lo peor es NPE, las fugas no son tan comunes. Pero para que esto funcione, necesita, como mínimo, un recolector de basura, que, a su vez, comienza a vivir su vida, en paralelo con el código de usuario, lo que reduce su previsibilidad. La máquina virtual aún proporciona independencia de la plataforma, pero cuánto se necesita es un punto discutible, no lo plantearé ahora.


Rust es un lenguaje de bajo nivel, el compilador genera un binario, que no requiere trucos adicionales para funcionar. Toda la lógica para eliminar objetos innecesarios está integrada en el código en el momento de la compilación, es decir. tampoco hay recolector de basura en tiempo de ejecución. Rust tampoco tiene referencias nulas y los tipos son seguros, lo que lo hace aún más confiable que Java.


En el núcleo de la gestión de la memoria está la idea de poseer una referencia de objeto y pedir prestado. Si solo una variable posee cada objeto, tan pronto como caduque al final del bloque, todo lo que señala puede borrarse recursivamente. Los enlaces también pueden ser prestados para leer o escribir. Aquí funciona el principio de un escritor y muchos lectores.


Este concepto se puede demostrar en el siguiente código. Se llama a Test () desde el método main () , que crea una estructura de datos recursiva MyStruct que implementa la interfaz destructor. Drop le permite configurar la lógica para que se ejecute antes de que se destruya el objeto. Algo similar al finalizador en Java, solo que a diferencia de Java, el momento de la llamada al método drop () es bastante seguro.


fn main() { test(); println!("End of main") } fn test() { let a = MyStruct { v: 1, s: Box::new( Some(MyStruct { v: 2, s: Box::new(None), }) ), }; println!("End of test") } struct MyStruct { v: i32, s: Box<Option<MyStruct>>, } impl Drop for MyStruct { fn drop(&mut self) { println!("Cleaning {}", self.v) } } 

La conclusión será la siguiente:


 End of test Cleaning 1 Cleaning 2 End of main 

Es decir antes de salir de test (), la memoria se borró recursivamente. El compilador se encargó de esto insertando el código necesario. Qué es Box y Option describirá un poco más tarde.


De esta manera, Rust toma la seguridad de los lenguajes de alto nivel y la previsibilidad de los lenguajes de programación de bajo nivel.


Que mas interesante


A continuación, enumero las características del lenguaje en orden descendente de importancia, en mi opinión.


Oop


Aquí el óxido generalmente está por delante del resto. Si la mayoría de los idiomas han llegado a la conclusión de que la herencia múltiple debe ser abandonada, entonces en Rust no hay herencia en absoluto. Es decir una clase solo puede implementar interfaces en cualquier cantidad, pero no puede heredar de otras clases. En términos de Java, esto significaría hacer que todas las clases sean finales. En general, la variedad sintáctica para mantener la POO no es tan grande. Quizás esto sea lo mejor.


Para combinar datos, hay estructuras que pueden contener implementación. Las interfaces se denominan rasgos y también pueden contener implementaciones predeterminadas. No llegan a clases abstractas, porque no puede contener campos; muchos se quejan de esta restricción. La sintaxis es la siguiente, creo que los comentarios no son necesarios aquí:


 fn main() { MyPrinter { value: 10 }.print(); } trait Printer { fn print(&self); } impl Printer { fn print(&self) { println!("hello!") } } struct MyPrinter { value: i32 } impl Printer for MyPrinter { fn print(&self) { println!("{}", self.value) } } 

De las características que noté, vale la pena señalar lo siguiente:


  • Las clases no tienen constructores. Solo hay inicializadores que especifican valores para campos a través de llaves. Si necesita un constructor, esto se hace a través de métodos estáticos.
  • El método de instancia difiere del estático al tener la auto referencia como primer argumento.
  • Las clases, interfaces y métodos también pueden generalizarse. Pero a diferencia de Java, esta información no se pierde en el momento de la compilación.

Algo más de seguridad


Como dije, Rust presta gran atención a la confiabilidad del código e intenta evitar la mayoría de los errores en la etapa de compilación. Para esto, se excluyó la capacidad de hacer que los enlaces estén vacíos. Me recordó a los tipos anulables de Kotlin. La opción se usa para crear enlaces vacíos. Al igual que en Kotlin, al intentar acceder a una variable de este tipo, el compilador golpeará las manos, obligando a insertar cheques. Intentar extraer el valor sin verificarlo puede provocar un error. Pero esto ciertamente no se puede hacer por accidente como, por ejemplo, en Java.


También me gustó el hecho de que todas las variables y campos de clase son inmutables por defecto. Hola de nuevo Kotlin. Si el valor puede cambiar, esto debe indicarse explícitamente con la palabra clave mut . Creo que el deseo de inmutabilidad mejora en gran medida la legibilidad y la previsibilidad del código. Aunque la opción por alguna razón es mutable, no entendí esto, aquí está el código de la documentación:


 let mut x = Some(2); let y = x.take(); assert_eq!(x, None); assert_eq!(y, Some(2)); 

Traslados


El óxido se llama enum . Solo además de un número limitado de valores pueden contener datos y métodos arbitrarios. Por lo tanto, es algo entre enumeraciones y clases en Java. La opción de enumeración estándar en mi primer ejemplo solo pertenece a este tipo:


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

Hay una construcción especial para procesar tales valores:


 fn main() { let a = Some(1); match a { None => println!("empty"), Some(v) => println!("{}", v) } } 

También


No tengo la intención de escribir un libro de texto sobre Rust, pero simplemente quiero enfatizar sus características. En esta sección describiré qué más es útil, pero, en mi opinión, no es tan único:


  • Los fanáticos de la programación funcional no se sentirán decepcionados; hay lambdas para ellos. El iterador tiene métodos para procesar la colección, por ejemplo, filter y for_each . Algo así como las secuencias de Java.
  • La construcción de coincidencia también se puede usar para cosas más complejas que la enumeración regular, por ejemplo, para procesar patrones.
  • Hay una gran cantidad de clases integradas, por ejemplo, colecciones: Vec, LinkedList, HashMap , etc.
  • Puedes crear macros
  • Es posible agregar métodos a clases existentes
  • Inferencia de tipo automática compatible
  • Junto con el lenguaje viene un marco de prueba estándar
  • La utilidad de carga incorporada se usa para construir y administrar dependencias

Volar en la pomada


Esta sección es necesaria para completar la imagen.


Problema asesino


El principal inconveniente proviene de la característica principal. Tienes que pagar por todo. En Rust, es muy inconveniente trabajar con estructuras de datos de gráficos mutables, porque cualquier objeto no debe tener más de un enlace. Para evitar esta limitación, hay un montón de clases integradas:


  • Box : un valor inmutable en el montón, un análogo de envoltorios para primitivas en Java
  • Celda - valor variable
  • RefCell - valor variable accesible por referencia
  • Rc - contador de referencia, para múltiples referencias a un objeto

Y esta es una lista incompleta. Para la primera muestra de Rust, decidí imprudentemente escribir una lista vinculada individualmente con métodos básicos. Finalmente, el enlace al nodo resultó en la siguiente Opción <Rc <RefCell <ListNode> >> :


  • Opción : para procesar un enlace vacío
  • Rc : para enlaces múltiples, como el último nodo está referenciado por el nodo anterior y la hoja en sí
  • RefCell - para enlace mutable
  • ListNode : el siguiente elemento en sí

Parece regular, un total de tres envoltorios alrededor de un objeto. El código para simplemente agregar un elemento al final de la lista es muy engorroso y hay cosas no obvias en él, como la clonación y los préstamos:


 struct ListNode { val: i32, next: Node, } pub struct LinkedList { root: Node, last: Node, } type Node = Option<Rc<RefCell<ListNode>>>; impl LinkedList { pub fn add(mut self, val: i32) -> LinkedList { let n = Rc::new(RefCell::new(ListNode { val: val, next: None })); if (self.root.is_none()){ self.root = Some(n.clone()); } self.last.map(|v| { v.borrow_mut().next = Some(n.clone()) }); self.last = Some(n); self } ... 

En Kotlin, lo mismo parece mucho más simple:


 public fun add(value: Int) { val newNode = ListNode(null, value); root = root ?: newNode; last?.next = newNode last = newNode; } 

Como descubrí más tarde, tales estructuras no son típicas de Rust, y mi código es completamente no idiomático. La gente incluso escribe artículos completos:



Aquí Rust sacrifica la legibilidad por seguridad. Además, estos ejercicios aún pueden conducir a enlaces en bucle que se cuelgan en la memoria, porque Ningún recolector de basura se los llevará. No escribí código de trabajo en Rust, así que es difícil para mí decir cuánto complican la vida tales dificultades. Sería interesante recibir comentarios de ingenieros en ejercicio.


Dificultad para aprender


El largo proceso de aprendizaje de Rust se sigue en gran medida de la sección anterior. Antes de escribir algo, debe dedicar tiempo a dominar el concepto clave de la propiedad de la memoria, como impregna cada línea. Por ejemplo, la lista más simple me llevó un par de noches, mientras que en Kotlin se escribe lo mismo en 10 minutos, aunque este no es mi idioma de trabajo. Además, muchos enfoques familiares para escribir algoritmos o estructuras de datos en Rust se verán diferentes o no funcionarán en absoluto. Es decir al cambiar a él, se requerirá una reestructuración más profunda del pensamiento, solo dominar la sintaxis no será suficiente. Esto está lejos de JavaScript, que se traga y aguanta todo. Creo que Rust nunca será el idioma que se les enseña a los niños en una escuela de programación. Incluso C / C ++ tiene más posibilidades en este sentido.


Al final


La idea de administrar la memoria en la etapa de compilación me pareció muy interesante. En C / C ++, no tengo experiencia, por lo que no lo compararé con el puntero inteligente. La sintaxis es generalmente agradable y no hay nada superfluo. Critiqué a Rust por la complejidad de implementar estructuras de datos gráficos, pero sospecho que esta es una característica de todos los lenguajes de programación que no son GC. Quizás la comparación con Kotlin no fue del todo honesta.


Todo


En este artículo, no toqué el subproceso múltiple en absoluto, creo que este es un gran tema por separado. Todavía hay planes para escribir algún tipo de estructura de datos o algoritmo más complicado que la lista, si tiene ideas, por favor comparta en los comentarios. Sería interesante saber qué tipos de aplicaciones se escriben generalmente en Rust.


Leer


Si está interesado en Rust, aquí hay algunos enlaces:



UPD: Gracias a todos por sus comentarios. Aprendí muchas cosas útiles para mí. Inexactitudes y errores tipográficos corregidos, enlaces agregados. Creo que tales discusiones contribuyen en gran medida al estudio de las nuevas tecnologías.

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


All Articles