¿Cuáles son las diferencias entre `String` y `str` de Rust?

Resuelto Daniel Fath asked hace 10 años • 15 respuestas

¿Por qué Rust tiene ambos Stringy str? ¿Cuáles son las diferencias entre ellos y cuándo se debe utilizar uno sobre el otro? ¿Alguno de ellos está quedando obsoleto?

Daniel Fath avatar Jun 11 '14 15:06 Daniel Fath
Aceptado

Stringes el tipo de cadena de montón dinámico, como Vec: úselo cuando necesite poseer o modificar sus datos de cadena.

stres una secuencia inmutable de 1 bytes UTF-8 de longitud dinámica en algún lugar de la memoria. Como se desconoce el tamaño, sólo se puede manejar detrás de un puntero. Esto significa que strlo más común es que 2 aparezca como &str: una referencia a algunos datos UTF-8, normalmente llamados "segmento de cadena" o simplemente "segmento". Un segmento es solo una vista de algunos datos, y esos datos pueden estar en cualquier lugar, por ejemplo

  • En almacenamiento estático : un literal de cadena "foo"es un archivo &'static str. Los datos se codifican en el ejecutable y se cargan en la memoria cuando se ejecuta el programa.

  • Dentro de un montón asignadoString : Stringdesreferencias a una &strvista de los Stringdatos de.

  • En la pila : por ejemplo, lo siguiente crea una matriz de bytes asignada por la pila y luego obtiene una vista de esos datos como&str :

    use std::str;
    
    let x: [u8; 3] = [b'a', b'b', b'c'];
    let stack_str: &str = str::from_utf8(&x).unwrap();
    

En resumen, utilícelo Stringsi necesita datos de cadena propios (como pasar cadenas a otros subprocesos o crearlos en tiempo de ejecución) y utilícelo &strsi solo necesita una vista de una cadena.

Esto es idéntico a la relación entre un vector Vec<T>y un segmento &[T], y es similar a la relación entre por valor Ty por referencia &Tpara tipos generales.


1 A stres de longitud fija; no puede escribir bytes más allá del final ni dejar bytes no válidos al final. Dado que UTF-8 es una codificación de ancho variable, esto efectivamente obliga a todos strlos s a ser inmutables en muchos casos. En general, la mutación requiere escribir más o menos bytes que los que había antes (por ejemplo, reemplazar a(1 byte) por ä(2+ bytes) requeriría hacer más espacio en el archivo str). Existen métodos específicos que pueden modificar un &mut strarchivo, principalmente aquellos que manejan solo caracteres ASCII, como make_ascii_uppercase.

2 Los tipos de tamaño dinámico permiten cosas como Rc<str>una secuencia de referencia contada en bytes UTF-8 desde Rust 1.2. Rust 1.21 permite crear fácilmente estos tipos.

huon avatar Jun 11 '2014 09:06 huon

Tengo experiencia en C++ y me resultó muy útil pensar Stringen &strtérminos de C++:

  • Un Rust Stringes como un std::string; es dueño de la memoria y hace el trabajo sucio de administrar la memoria.
  • Un Rust &stres como un char*(pero un poco más sofisticado); nos señala el comienzo de un fragmento de la misma manera que puede obtener un puntero al contenido de std::string.

¿Alguno de ellos va a desaparecer? No lo creo. Tienen dos propósitos:

StringMantiene el buffer y es muy práctico de usar. &stres liviano y debe usarse para "mirar" las cadenas. Puede buscar, dividir, analizar e incluso reemplazar fragmentos sin necesidad de asignar nueva memoria.

&strPuede mirar dentro de a Stringya que puede apuntar a alguna cadena literal. El siguiente código necesita copiar la cadena literal en la Stringmemoria administrada:

let a: String = "hello rust".into();

El siguiente código le permite usar el literal sin una copia (aunque de solo lectura):

let a: &str = "hello rust";
Luis Ayuso avatar Jun 07 '2017 08:06 Luis Ayuso

Es strlo que es análogo a String, no su porción.

An stres una cadena literal, básicamente un texto preasignado:

"Hello World"

Este texto debe almacenarse en algún lugar, por lo que se almacena en la sección de datos del archivo ejecutable junto con el código de máquina del programa, como una secuencia de bytes ([u8]).

┌─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┐
│  H  │  e  │  l  │  l  │  o  │     │  W  │  o  │  r  │  l  │  d  │
└─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┘
┌─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┐
│  721011081081113287111114108100 │
└─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┘

Dado que el texto puede tener cualquier longitud, su tamaño tiene un tamaño dinámico.

Ahora que almacenamos el texto, necesitamos una forma de acceder a él y ahí es donde entra en juego el segmento.

Un segmento , [T]es una vista de un bloque de memoria. Sea mutable o no, un segmento siempre toma prestado y es por eso que siempre está detrás de un puntero ,.&

Expliquemos el significado de tener un tamaño dinámico.

Algunos lenguajes de programación, como C, añaden un byte cero ( \0) al final de sus cadenas y mantienen un registro de la dirección inicial. Para determinar la longitud de una cadena, el programa debe recorrer los bytes sin procesar desde la posición inicial hasta encontrar este byte cero.

Sin embargo, Rust adopta un enfoque diferente: utiliza un segmento. Un segmento almacena la dirección donde strcomienza y cuántos bytes ocupa. Es mejor que agregar cero bytes porque el cálculo se realiza por adelantado durante la compilación.

El tamaño del texto se puede conocer de antemano, pero aún cambia con los datos subyacentes, lo que hace que su tamaño sea dinámico.

Si volvemos a la expresión "Hola mundo", devuelve un puntero grueso que contiene tanto la dirección de los datos reales como su longitud. Este puntero será nuestro identificador de los datos reales y también se almacenará en nuestro programa. Ahora los datos están detrás de un puntero y el compilador conoce su tamaño en el momento de la compilación.

Dado que el texto se almacena en el código fuente, será válido durante toda la vida útil del programa en ejecución y, por lo tanto, tendrá toda la staticvida útil.

Entonces, el valor de retorno de la expresión "Hola Palabra" debe reflejar estas dos características, y así es:

let s: &'static str = "Hello World";

Quizás se pregunte por qué su tipo se escribe como strpero no como [u8], porque siempre se garantiza que los datos serán una secuencia UTF-8 válida. No todos los caracteres UTF-8 son de un solo byte, algunos ocupan 4 bytes. Entonces [u8] sería inexacto.

Si desmonta un programa Rust compilado e inspecciona el archivo ejecutable, verá varios strmensajes de correo electrónico almacenados uno al lado del otro en la sección de datos sin ninguna indicación de dónde comienza uno y termina el otro.

El compilador va un paso más allá: si se utiliza texto estático idéntico en varias ubicaciones del programa, el compilador de Rust optimizará el programa creando un único bloque binario para todos los valores duplicados.

Por ejemplo, el compilador crea un único binario continuo con el contenido de "Hello World" para el siguiente código aunque usemos tres literales diferentes con "Hello World":

let x: &'static str = "Hello World";
let y: &'static str = "Hello World";
let z: &'static str = "Hello World";

String, por otro lado, es un tipo especializado que almacena su valor como vector de u8. Eche un vistazo a cómo Stringse define el tipo en el código fuente:

pub struct String {
    vec: Vec<u8>,
}

Ser vectorial significa que está asignado al montón y cuyo tamaño se puede cambiar como cualquier otro valor vectorial.

Sin embargo, si observa con atención, verá que vecel campo se mantiene privado. Al ser un medio privado, no podemos crear una instancia de String directamente sino a través de los métodos proporcionados. La razón por la que se mantiene privado es porque no todos los flujos de bytes producen caracteres utf-8 válidos y la interacción directa con los bytes subyacentes puede dañar los datos. A través de este compilador de acceso controlado se garantiza que los datos sean válidos y sigan siendo válidos.

La palabra especializada en la definición de tipo se refiere a esta característica, característica de no permitir el acceso arbitrario pero sí imponer ciertos controles sobre los datos a través de un acceso controlado para brindar ciertas garantías. Aparte de eso, es sólo un vector.

En resumen, a Stringes un búfer de tamaño variable que contiene texto UTF-8. Este búfer se asigna en el montón, por lo que puede crecer según sea necesario o solicitado. Podemos llenar este búfer o cambiar su contenido como mejor nos parezca.

Hay varios métodos definidos en el tipo String para crear una instancia de String, nuevo es uno de ellos:

pub const fn new() -> String {
  String { vec: Vec::new() }
}

Podemos usarlo para crear una cadena válida.

let s = String::new();
println("{}", s);

Lamentablemente no acepta parámetros de entrada. Por lo tanto, el resultado será válido pero será una cadena vacía, pero crecerá como cualquier otro vector cuando la capacidad no sea suficiente para contener el valor asignado. Pero el rendimiento de las aplicaciones se verá afectado, ya que el crecimiento requiere una reasignación.

Podemos llenar el vector subyacente con valores iniciales de diferentes fuentes:

De una cadena literal

let a = "Hello World";
let s = String::from(a);

Tenga en cuenta que strtodavía se crea un an y su contenido se copia al vector asignado del montón a través de String.from. Si verificamos el binario ejecutable veremos bytes sin procesar en la sección de datos con el contenido "Hola mundo". Este es un detalle muy importante que algunas personas pasan por alto.

De partes crudas

let ptr = s.as_mut_ptr();
let len = s.len();
let capacity = s.capacity();

let s = String::from_raw_parts(ptr, len, capacity);

De un personaje

let ch = 'c';
let s = ch.to_string();

Del vector de bytes

let hello_world = vec![72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100];
// We know it is valid sequence, so we can use unwrap
let hello_world = String::from_utf8(hello_world).unwrap();
println!("{}", hello_world); // Hello World

Aquí tenemos otro detalle importante. Un vector puede tener cualquier valor, no hay garantía de que su contenido sea un UTF-8 válido, por lo que Rust nos obliga a tener esto en cuenta devolviendo un archivo Result<String, FromUtf8Error>en lugar de un archivo String.

Desde el búfer de entrada

use std::io::{self, Read};

fn main() -> io::Result<()> {
    let mut buffer = String::new();
    let stdin = io::stdin();
    let mut handle = stdin.lock();

    handle.read_to_string(&mut buffer)?;
    Ok(())
}

O de cualquier otro tipo que implemente ToStringrasgo

Dado que Stringes un vector subyacente, exhibirá algunas características de vector:

  • un puntero: el puntero apunta a un búfer interno que almacena los datos.
  • longitud: la longitud es el número de bytes almacenados actualmente en el búfer.
  • Capacidad: La capacidad es el tamaño del buffer en bytes. Por lo tanto, la longitud siempre será menor o igual a la capacidad.

Y delega algunas propiedades y métodos a los vectores:

pub fn capacity(&self) -> usize {
  self.vec.capacity()
}

La mayoría de los ejemplos usan String::from, lo que confunde a la gente al pensar por qué crear una cadena a partir de otra cadena.

Es una lectura larga, espero que ayude.

snnsnn avatar Sep 13 '2020 18:09 snnsnn

str, solo usado como &str, es un segmento de cadena, una referencia a una matriz de bytes UTF-8.

Stringes lo que solía ser ~str, una matriz de bytes UTF-8 de propiedad cultivable.

Chris Morgan avatar Jun 11 '2014 09:06 Chris Morgan

En realidad son completamente diferentes. En primer lugar, a strno es más que una cuestión de nivel de tipo; sólo se puede razonar a nivel de tipo porque es el llamado tipo de tamaño dinámico (DST). El tamaño que strocupa no se puede conocer en el momento de la compilación y depende de la información del tiempo de ejecución; no se puede almacenar en una variable porque el compilador necesita saber en el momento de la compilación cuál es el tamaño de cada variable. A stres conceptualmente solo una fila de u8bytes con la garantía de que forma UTF-8 válido. ¿Qué tan grande es la fila? Nadie lo sabe hasta el tiempo de ejecución, por lo que no se puede almacenar en una variable.

Lo interesante es que uno &stro cualquier otro puntero a strMe gusta Box<str> existe en tiempo de ejecución. Este es el llamado "puntero gordo"; es un puntero con información adicional (en este caso, el tamaño del objeto al que apunta), por lo que es el doble de grande. De hecho, a &strestá bastante cerca de a String(pero no de a &String). A &strson dos palabras; un puntero al primer byte de a stry otro número que describe cuántos bytes tiene str.

Al contrario de lo que se dice, a strno tiene por qué ser inmutable. Si puede obtener a &mut strcomo puntero exclusivo a str, puede mutarlo y todas las funciones seguras que lo mutan garantizan que se cumpla la restricción UTF-8 porque si se viola entonces tenemos un comportamiento indefinido ya que la biblioteca asume que esta restricción es verdadero y no lo comprueba.

Entonces, ¿qué es un String? Son tres palabras; dos son iguales que for &strpero agrega una tercera palabra que es la capacidad del strbuffer en el montón, siempre en el montón (a strno está necesariamente en el montón) lo administra antes de llenarlo y tiene que reasignarlo. StringBásicamente posee un como strdicen; lo controla y puede cambiar su tamaño y reasignarlo cuando lo considere oportuno. Entonces, Stringcomo se dijo, a está más cerca de a &strque de a str.

Otra cosa es un Box<str>; esto también posee a stry su representación en tiempo de ejecución es la misma que a &strpero también posee strdiferente a &strpero no puede cambiar su tamaño porque no conoce su capacidad, por lo que básicamente a Box<str>puede verse como una longitud fija Stringque no se puede cambiar de tamaño (puede conviértalo siempre en a Stringsi desea cambiar su tamaño).

Existe una relación muy similar entre [T]y Vec<T>excepto que no hay restricción UTF-8 y puede contener cualquier tipo cuyo tamaño no sea dinámico.

El uso de stra nivel de tipo es principalmente para crear abstracciones genéricas con &str; existe en el nivel de tipo para poder escribir rasgos cómodamente. En teoría, strcomo tipo, no era necesario que existiera y solo &streso significaría que se tendría que escribir una gran cantidad de código adicional que ahora puede ser genérico.

&stres muy útil poder tener múltiples subcadenas diferentes de a Stringsin tener que copiar; como se dijo, a es String propietario del strmontón que administra y si solo pudiera crear una subcadena de a Stringcon una nueva, Stringtendría que copiarse porque todo en Rust solo puede tener un único propietario para ocuparse de la seguridad de la memoria. Entonces, por ejemplo, puedes cortar una cadena:

let string: String   = "a string".to_string();
let substring1: &str = &string[1..3];
let substring2: &str = &string[2..4];

Tenemos dos subcadenas diferentes strde la misma cadena. stringes el que posee el strbúfer completo real en el montón y las &strsubcadenas son solo punteros gruesos a ese búfer en el montón.

Zorf avatar Jul 27 '2018 20:07 Zorf