Trabajando con bytes en Dart
Published: 2021-01-13 15:04:49

> Si puedes entender los bytes, puedes entender cualquier cosa.

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/u2am71ja9rfk5i22y41i.png)

Esta es una traducción al español del artículo [Working with bytes in Dart](https://medium.com/flutter-community/working-with-bytes-in-dart-6ece83455721) del autor [Suragch](https://suragch.medium.com/).
 

## Prefacio

Empecé con este tema porque estaba investigando cómo comunicarme entre Dart y un servidor de base de datos PostgreSQL. Resultó ser mucho más de bajo nivel de lo que esperaba. Pensé en escribir un artículo corto explicando algunas de las nuevas cosas que estaba aprendiendo. Bueno, tres días completos después, ese corto artículo se ha convertido en una de las explicaciones más profundas que he escrito sobre un tema que ya es un nicho. Aunque el artículo es largo, no creo que lo encuentres aburrido, y es muy probable que aprendas una o dos cosas nuevas sobre Dart aunque lo hayas estado usando durante un tiempo. Desde luego que sí. Como siempre, por favor, hágame saber si encuentra algún error. Así es como aprendo. Y pruebe los ejemplos de código usted mismo. Así es como se aprende.

Este artículo está actualizado para Dart 2.10.

## Bits y bytes

Todos los que leen este artículo saben que un `byte` es ocho `bits`:

```
00101010
```

Ese byte de `8-bit` tiene un valor, un valor de `42` en este caso, que es sólo un `integer`. Ahora, combinando ese conocimiento con el hecho de que todos los datos binarios son sólo una secuencia de bytes, esto significa que es posible representar cualquier dato binario como una lista de enteros en Dart:

```dart
List<int> data = [102, 111, 114, 116, 121, 45, 116, 119, 111, 0];
```

Esos bytes pueden ser de un archivo, una imagen de mapa de bits, una grabación mp3, un *memory dump*, una solicitud de red, o los códigos de caracteres de una cadena. Todos son bytes.

## Siendo un poco más eficiente

En Dart el tipo ``int`` tiene un valor por defecto de `64-bit`. Eso son ocho bytes. Aquí está el número `42` de nuevo, esta vez mostrando los 64 bits para su referencia visual:

```
0000000000000000000000000000000000000000000000000000000000101010
```

Si miras con cuidado, puedes notar que muchos de esos bits no están siendo usados.

Un `int` puede almacenar valores tan grandes como 9.223.372.036.854.775.807, pero lo más grande que será un byte es 255. Este es definitivamente un caso de usar una escopeta para matar una mosca. Ahora imagina un simple archivo de 1 megabyte (1024 bytes) como una lista de enteros. Tu problema se acaba de hacer un millón de veces más grande.

Aquí es donde entra en juego `Uint8List`. Este tipo es básicamente como `List<int>`, pero  es mucho más eficiente que para listas grandes. `Uint8List` es una lista de números enteros en la que los valores son sólo 8 bits cada uno, o 1 byte. La `U` de `Uint8List` significa `unsigned`, por lo que los valores van de 0 a 255. ¡Eso es perfecto para representar datos binarios!

## Nota al margen: Los números negativos en binario

¿Has pensado alguna vez en cómo representar los números negativos en binario? Bueno, la forma de hacerlo es hacer que el bit ubicado más a la izquierda sea un 1 para los negativos y un 0 para los positivos. Por ejemplo, estos `8-bit` enteros con signo son todos negativos porque todos comienzan con 1:

```
11111101
10000001
10101010
```

Por otro lado, los siguientes números enteros de `8-bit` con signo son todos positivos porque el bit ubicado más a la izquierda es 0 :

```
01111111
00000001
01010101
```

Podrías pensar que si 00000001 es +1, entonces 10000001 debería ser -1. Sin embargo, no funciona así. De lo contrario, tendrías dos valores para el cero: 10000000 y 00000000. La solución es usar un sistema llamado *Two's Complement*. El siguiente vídeo lo explica muy bien si estas interesado.

https://youtu.be/mRvcGijXI9w

En Dart ya se puede utilizar el tipo `int` para representar valores con signo, tanto positivos como negativos. Sin embargo, si quieres listas de sólo `8-bit` enteros con signo, puedes usar el tipo `Int8List` (nota la ausencia de `U`). Esto permite valores de -128 a 127. Sin embargo, esto no es particularmente útil para la mayoría de los casos de uso, así que nos quedaremos con los enteros signo en `Uint8List`.

## Convirtiendo `List<int>` a `Uint8List`

`Uint8List` forma parte de `dart:typed_data`, una de las principales bibliotecas de Dart. Para utilizarla, añada la siguiente importación:

```dart
import 'dart:typed_data';
```

Ahora puede convertir la lista `List<int>` que tenía antes en `Uint8List` utilizando el método `fromList`:

```dart
List<int> data = [102, 111, 114, 116, 121, 45, 116, 119, 111, 0];

Uint8List bytes = Uint8List.fromList(data);
```

Si alguno de los valores de la lista `List<int>` está fuera del rango de 0 a 255, entonces los valores se ajustarán. Puedes ver eso en el siguiente ejemplo:

```dart
List<int> data = [3, 256, -2, 2348738473];

Uint8List bytes = Uint8List.fromList(data); 

print(bytes); // [3, 0, 254, 169]
```

256 fue uno más allá de 255, así que se convirtió en 0. El siguiente número, -2, es menos que 0, así que retrocedió dos posiciones en la parte superior y se convirtió en 254. Y no tengo ni idea de cuántas vueltas dio el 2348738473, pero finalmente terminó en 169.

Si no quieres que se produzca este comportamiento, puedes usar `Uint8ClampedList` en su lugar. Esto sujetará todos los valores mayores de 255 a 255 y todos los valores menores de 0 a 0.

```dart
List<int> data = [3, 256, -2, 2348738473];

Uint8ClampedList bytes = Uint8ClampedList.fromList(data);

print(bytes); // [3, 255, 0, 255]
```

Esta vez los tres valores finales fueron fijados.

## Creando un `Uint8List`

En el método anterior se creó un `Uint8List` convirtiendo un `List<int>`. Pero, ¿y si sólo quieres empezar con una lista de bytes? Puedes hacerlo pasando la longitud de la lista al constructor así:

```dart
final byteList = Uint8List(128);
```

Esto crea una lista de longitud fija donde los 128 valores son 0. Imprime `byteList` y verás:

```
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
```

Siéntase libre de contarlos para asegurarse de que realmente hay 128.

## Modificando una lista de bytes

¿Cómo se modifican los valores de un `Uint8List`? Como lo harías con una lista normal:

```dart
byteList[2] = 255;
```

La impresión `byteList` muestra de nuevo que el valor de index 2 ha cambiado:

```
[0, 0, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
```

La naturaleza de acceso aleatrorio de las listas hace que sea rápido y conveniente modificar el valor del byte en cualquier ubicación del index.

## Listas ampliables (growable)

La lista que hizo arriba fue `fixed-length`. Si intentabas hacer algo como esto:

```dart
final byteList = Uint8List(128);

byteList.add(42);
```

...obtendrías la siguiente excepción:

```dart
Unsupported operation: Cannot add to a fixed-length list
```

Eso no es muy conveniente si estás tratando de hacer algo como recoger los bytes de un `stream`. Para hacer una lista de bytes `growable`, necesitas un `BytesBuilder`:

```dart
final bytesBuilder = BytesBuilder();
bytesBuilder.addByte(42);
bytesBuilder.add([0, 5, 255]);
```

Se puede añadir un solo bytes (del tipo `int`) o listas de bytes (del tipo `List<int>`).

Cuando quiera convertir el constructor a `Uint8List`, utilice el método `toBytes` así:

```dart
Uint8List byteList = bytesBuilder.toBytes();
print(byteList); // [42, 0, 5, 255]
```

## Más de una forma de ver los bits

Bits y bytes son sólo un montón de ceros y unos. No tienen mucho significado si no tienes una forma de interpretarlos: 

```
0011111100011101110011011101101110011111100110011011110000010101001110100010101010000111100101101111001111011100000101011001101111010101101000011101100101000111100000100100011111101011111110100111010011001100000000000111111010010010000000001111010001100000 
```

Con `Uint8List` la forma en que los interpretamos es diciendo que cada 8 bits hay un número entre 0 y 255:

```
00111111 00011101 11001101 11011011 10011111 10011001 10111100 00010101 00111010 00101010 10000111 10010110 11110011 11011100 00010101 10011011 11010101 10100001 11011001 01000111 10000010 01000111 11101011 11111010 01110100 11001100 00000000 01111110 10010010 00000000 11110100 01100000 
```

Podrías reinterpretar esos mismos valores como números enteros con signo de -128 a 127. Eso es lo que hicimos con `Int8List`.

Sin embargo, también hay otras formas de ver los datos. Por ejemplo, en lugar de usar trozos de `8-bit`, podrías mirar los datos como una lista de trozos de `16-bit`:

```
0011111100011101 1100110111011011 1001111110011001 1011110000010101 0011101000101010 1000011110010110 1111001111011100 0001010110011011 1101010110100001 1101100101000111 1000001001000111 1110101111111010 0111010011001100 0000000001111110 1001001000000000 1111010001100000
```

La biblioteca `dart:typed_data` tiene tipos para eso también. Usa `Uint16List` para los enteros `unsigned` que van de 0 a 65.535, o `Int16List` para los enteros con signo que van de -32.768 a 32.767.

No se detiene ahí. También podrías interpretar esos mismos datos como una lista de valores `32-bit`:


```
00111111000111011100110111011011 10011111100110011011110000010101 00111010001010101000011110010110 11110011110111000001010110011011 11010101101000011101100101000111 10000010010001111110101111111010 01110100110011000000000001111110 10010010000000001111010001100000
```

O los valores `64-bit`:

```
0011111100011101110011011101101110011111100110011011110000010101 

0011101000101010100001111001011011110011110111000001010110011011 

1101010110100001110110010100011110000010010001111110101111111010 

0111010011001100000000000111111010010010000000001111010001100000
```

O incluso valores de `128-bit`:

```
00111111000111011100110111011011100111111001100110111100000101010011101000101010100001111001011011110011110111000001010110011011 

11010101101000011101100101000111100000100100011111101011111110100111010011001100000000000111111010010010000000001111010001100000
```

Dart tiene tipos para todos ellos:

- `Int32List`
- `Uint32List`
- `Int64List`
- `Uint64List`
- `Int32x4List` (128 bits)

Usar el tipo de lista específica que necesita será más eficiente para grandes cantidades de datos que usar `List<int>`.

## Vistas en bytes en Dart

Dart respalda *raw binary data* con un `ByteBuffer`. Todos los tipos que se vieron en la última sección implementan una clase llamada `TypedData`, que es sólo una forma genérica de ver los datos en el `ByteBuffer`. Esto significa que tipos como `Uint8List`, `Int32List`, y `Uint64List` son todas formas diferentes de ver los mismos datos.

Usaremos la siguiente lista de cuatro bytes para los ejemplos que vienen:

```
00000001 00000000 00000000 10000000
```

En forma decimal la lista se vería así:

```
1, 0, 0, 128
```

Primero crea la lista en Dart como lo has hecho anteriormente:

```dart
Uint8List bytes = Uint8List.fromList([1, 0, 0, 128]);
```

Como con cualquier forma de `TypedData`, se puede acceder a la propiedad subyacente `ByteBuffer` de un `Uint8List` accediendo a la propiedad `buffer`:

```dart
ByteBuffer byteBuffer = bytes.buffer;
```

### Unsigned 16-bit view

Ahora que tienes el byte buffer, puedes obtener una vista diferente de los bytes usando uno de los métodos `as...`, esta vez `asUint16List`:

```dart
Uint16List sixteenBitList = byteBuffer.asUint16List();
```

Antes de imprimir `sixteenBitList` para ver el contenido, ¿cuáles cree que serán los valores?

¿Crees que sabes la respuesta? Bien, imprime la lista:

```dart
print(sixteenBitList);
```

En mi ordenador Mac los resultados son los siguientes:

```
[1, 32768]
```

Eso es muy extraño. Ya que los valores originales eran:

```
00000001 00000000 00000000 10000000
1        0        0        128
```

Hubiera esperado que se combinaran en trozos de `16-bit` como este:

```
0000000100000000 0000000010000000
256              128
```

En cambio, tenemos esto:

```
0000000000000001 1000000000000000
1                32768
```

Mantén ese pensamiento. Revisemos la vista de `32-bit`.

### Unsigned 32-bit view

Empezaré con la lista de bytes que contiene los valores decimales 0, 1, 2, 3. De esta manera podemos ver si están en el mismo orden o no. Para mayor claridad, aquí está el aspecto que tendrá la lista original en forma binaria `8-bit`:

```
00000000 00000001 00000010 00000011
0        1        2        3
```

Ahora ejecuta el siguiente código:

```dart
Uint8List bytes = Uint8List.fromList([0, 1, 2, 3]);
ByteBuffer byteBuffer = bytes.buffer;
Uint32List thirtytwoBitList = byteBuffer.asUint32List();
print(thirtytwoBitList);
```

Esta vez tienes una vista `32-bit` en el buffer subyacente del `Uint8List` original. La declaración impresa revela un valor de 50462976, que en `32-bit` binario es:

```
00000011000000100000000100000000
```

o si se añade un espacio (para ayudar a ver las partes):

```
00000011 00000010 00000001 00000000
```

¡Eso es exactamente el orden inverso al original! ¿Qué es lo que está pasando?

## Endianness

Normalmente no tienes que pensar en este tipo de cosas cuando todo lo que haces es construir felizmente una aplicación Flutter, pero cuando caes tan bajo como lo estamos haciendo hoy, en realidad estás chocando con la arquitectura de la máquina.

Algunas máquinas ordenan los bytes individuales dentro de un trozo de bytes (ya sean trozos de 2-byte, trozos de 4-byte o trozos de 8-byte) en orden ascendente. Esto se llama *big endian* porque el byte más significativo es el primero. Eso es normalmente lo que esperamos porque leemos los números de izquierda a derecha, empezando por la parte más grande del número.

Sin embargo, otras máquinas ordenan los bytes individuales dentro de un trozo de bytes en orden inverso. Esto se llama *little endian*. Aunque parezca poco intuitivo, funciona en el nivel de la arquitectura de la máquina. El problema viene cuando *big endian* se encuentra con *little endian*. Y eso es lo que nos pasó a nosotros arriba.

Antes de hablar más sobre eso, quizás quieran ver este video en *endianess*. La explicación es bastante buena (si puedes pasar de la pequeña historia del principio):

https://youtu.be/_wk_nZVuY0Q

### Donde ocurrió el malentendido

Cuando corrí el código:

```dart
Uint8List bytes = Uint8List.fromList([0, 1, 2, 3]);

ByteBuffer byteBuffer = bytes.buffer;

Uint32List thirtytwoBitList = byteBuffer.asUint32List();

print(thirtytwoBitList);
```

Dart tomó la lista de estos cuatro bytes:

```
00000000 00000001 00000010 00000011
```

Y se las dio a mi ordenador. Dart sabía que 00000000 era el primero, 00000001 era el segundo, 00000010 era el tercero y 00000011 era el último. Mi ordenador también lo sabía.

Entonces Dart pidió a mi computadora una vista de trozos de 32 bits, de esa lista de bytes, del buffer de bytes de la memoria. Mi computadora devolvió:

```
00000011000000100000000100000000
```

Bueno, resulta que mi MacBook tiene una arquitectura *little endian*. Mi ordenador todavía sabía que 00000000 era el primero, 00000001 era el segundo, 00000010 era el tercero y 00000011 era el último. Sin embargo, cuando la declaración `print` recurrió a algún método `toString` en algún lugar, interpretó esos 32 bits como un solo integer en formato `big endian`. No lo culpo.

### Comprobando endianness en Dart

Si quiere saber cuál es la arquitectura endian de su sistema, puede ejecutar el siguiente código en Dart:

```dart
print(Endian.host == Endian.little);
```

Si eso es `true`, entonces usted también tiene una *little endian*. De lo contrario, es *big endian*. Lo más probable es que la suya sea *little endian*, ya que la mayoría de los ordenadores personales de hoy en día la utilizan.

Si miran el código fuente de la clase `Endian`, verán que la forma en que comprueba la arquitectura del host es muy similar a la que me llevó a mi sorpresa inicial:

```dart
class Endian {
  ...
  static final Endian host =
      (new ByteData.view(new Uint16List.fromList([1]).buffer))
        .getInt8(0) == 1
          ? little
          : big;
}
```

Toma una representación `16-bit` de 1 y luego ve si los primeros 8 bits son 1, en cuyo caso es *little endian*. Eso es porque los *little endian* ordenan esos 16 bits así:

```
00000001 00000000
```

mientras que las máquinas *big endian* las ordenan así:

```
00000000 00000001
```

### Por qué debería importarme?

Puede que pienses que no necesitas preocuparte por esto, ya que no planeas acercarte tanto a bajo nivel. Sin embargo, *endianness* aparece en otros contextos, también. Básicamente, cada vez que trabajes con datos compartidos que lleguen en algo más grande que trozos de `8-bit`, debes tener en cuenta *endianess*.

Por ejemplo, si has visto el vídeo que he enlazado arriba, sabes que los diferentes formatos de archivo utilizan una codificación diferente de *endian*:

- JPEG (big endian)
- GIF (little endian)
- PNG (big endian)
- BMP (little endian)
- MPEG-4 (big endian)
- Network data (big endian)
- UTF-16 text files (big or little endian — watch this video)

Así que incluso si conoces el *endianness* del raw byte data con el que estás trabajando, ¿cómo le comunicas eso al Dart para que no tengas el mismo problema de falta de comunicación que yo tuve antes?

Sigue leyendo para averiguarlo.

### Manejando endianness en Dart

Una vez que se tienen los datos en bruto como alguna clase `TypedData` como podría ser `Uint8List`, se puede usar la clase `ByteData` para hacer tareas de acceso de lectura y escritura en ella. De esta manera no tienes que interactuar con el `BytesBuffer` directamente. `ByteData` también permite especificar el *endianess* con el que se desea interpretar los datos.

Escriba las siguientes líneas de código:

```dart
Uint8List byteList = Uint8List.fromList([0, 1, 2, 3]);

ByteData byteData = ByteData.sublistView(byteList);

int value = byteData.getUint16(0, Endian.big);

print(value);
```

Esto es lo que hace:

- Obtienes tus datos tipados `Uint8List` de una lista de números enteros. Esto es lo mismo que has visto anteriormente.
- Luego lo envuelves con `ByteData`. Una view (o "sublist" view) es una forma de ver los datos en el *buffer*. La razón por la que se llama *sublista* es que no es necesario tener una vista de todo el *buffer*, que podría ser muy grande. Es posible ver un rango más pequeño de bytes dentro del *buffer*. En nuestro caso, sin embargo, el buffer para `byteList` sólo incluye esos cuatro bytes, así que estamos viendo todo.
- Después de eso se accede a 16 bits (o dos bytes) comenzando en index 0, donde la indexación es en incrementos de *one-byte*. Si eliges 1 como index obtendrás el segundo y tercer bytes, y si eliges 2 como index obtendrás el tercero y cuarto bytes. Cualquier index más alto que 2 en este caso arrojaría un error ya que sólo hay cuatro bites disponibles.
- Por último, también se le dice a Dart que se desea interpretar esos dos bytes que comienzan en index 0 como *big endian*. Ese es el valor por defecto de `ByteData`, así que podrías haber omitido ese parámetro.

Ejecuta el código y verás una salida de 1. Eso tiene sentido porque

```
00000000 00000001
```

interpretado de manera *big endian* tiene un valor de 1.

Hágalo de nuevo y sustituya `Endian.big` por `Endian.little`. Ahora, cuando ejecute el código verá 256 porque está interpretando el segundo byte (00000001) como anterior al primero (00000000), así que obtendrá

```
0000000100000000
```

que no es lo que quieres para estos datos, así que cambia el código de nuevo a `Endian.big`.

### Setting bytes in ByteData

En la sección de arriba estabas recibiendo bytes. También puedes settear bytes mientras especificas el *endianess* de casi la misma manera en que los obtuviste. Añade la siguiente línea a lo que escribiste anteriormente:

```dart
byteData.setInt16(2, 256, Endian.big);

print(byteList);
```

Aquí está poniendo dos bytes (con un valor de 256) comenzando en index 2 en el buffer. Imprime eso y verás:

```
[0, 1, 1, 0]
```

¿Tiene sentido? Anteriormente el buffer contenía:

```
[0, 1, 2, 4]
```

Pero reemplazaste los dos últimos bytes con 256, que en binario es:

```
00000001 00000000
```

o 1 y 0, cuando se interpreta de manera *big endian*.

Bien, es suficiente sobre *endianness*. La mayoría de las veces puedes aceptar el valor por defecto de *big endian* y no pensar en ello.

## Hexadecimal y binario

Hasta ahora he estado escribiendo todos los números binarios de este artículo como strings como 10101010, pero no es muy conveniente usar ese formato al escribir el código porque Dart tiene por defecto la base 10 para los números enteros.

```dart
int x = 10000000;
```

Son diez millones, no 128.

Aunque puede ser difícil escribir directamente en binario, es bastante fácil escribir directamente valores hexadecimales en Dart. Sólo hay que anteponer al número *hex* el prefijo `0x` así:

```dart
int x = 0x80;  // 10000000 binary
print(x);      // 128 decimal
```

Como la relación entre binario y hex es tan directa, eso hace que convertir entre los dos sea una tarea fácil. Si trabajas mucho con binario, puede que valga la pena memorizar la tabla de conversión:

```
hex  binary 
----------
0    0000
1    0001
2    0010
3    0011
4    0100
5    0101
6    0110
7    0111
8    1000
9    1001
A    1010
B    1011
C    1100
D    1101
E    1110
F    1111
```

Los valores de los bytes siempre estarán en dos trozos de valor hex (o *nibbles* como se llama técnicamente a un trozo de 4-bit, oh el humor de esos primeros programadores).

Aquí hay algunos ejemplos más. Si cubres el binario y sólo miras el hex, ¿puedes descifrar el binario sin mirar?

```
hex       binary 
----------------
80        1000 0000
2A        0010 1010
F2F2F2    1111 0010 1111 0010 1111 0010
1F60E     0001 1111 0110 0000 1110
```

Como trivialidad, el tercer valor es el valor RGB (red green blue) hex para el tono de gris que Medium utiliza como backgroundd color para los bloques de código. Y el último es el punto de código Unicode del emoji con cara sonriente y gafas de sol 😎.

### Convirtiendo hex y binario strings

Un truco divertido en Dart que tal vez no conozcas es que puedes convertir entre diferentes bases y obtener la representación en cadena de los valores.

Aquí hay un ejemplo de tomar un número decimal y convertirlo a hex y binario en forma de cadena:

```dart
String hex = 2020.toRadixString(16).padLeft(4, '0');
print(hex); // 07e4

String binary = 2020.toRadixString(2);
print(binary); // 11111100100
```

Notas:

- Radix sólo significa la base.
- El primer ejemplo toma el número decimal 2020, lo convierte en base-16 (es decir, hexadecimal), y se asegura de que la longitud sea 4. Cualquier longitud menor de 4 se rellena con ceros a la izquierda. Esto hizo que 7e4 se convirtiera en 07e4.
- El segundo ejemplo convierte el decimal 2020 a binario en el formato String.

Puedes hacer la conversión de la otra manera usando `int.parse`:

```dart
int myInt = int.parse('07e4', radix: 16);
print(myInt); // 2020

myInt = int.parse('11111100100', radix: 2);
print(myInt); // 2020
```

## Convertir Unicode strings en bytes y volver

Cuando digo bytes aquí, sólo me refiero a números. El tipo String en Dart es una lista de números *Unicode*. Los números Unicode se llaman puntos de código y pueden ser tan pequeños como 0 o tan grandes como 10FFFF. Aquí hay un ejemplo:

```
character    Unicode code point (hex)
-------------------------------
H            48
e            65
l            6C
l            6C
o            6F
😎           1F60E
```

La palabra que Dart usa para los puntos de código es *runes*. Puedes verlos así:

```dart
Runes codePoints = 'Hello😎'.runes;

print(codePoints); // (72, 101, 108, 108, 111, 128526)
```

Esa es la versión decimal de sus valores hex.

Runes son un iterable de valores `int`. Sin embargo, tal y como se vio al principio del artículo con `List<int>` y `Uint8List`, `64` o incluso `32 bits` no es muy eficiente para almacenar texto cuando la mayoría de los caracteres del mundo de habla inglesa (además de emojis como 😎) sólo toman 8 bits. Incluso la mayoría de los miles y miles de caracteres chinos que existen pueden ser representados con menos de 16 bits.

Por esa razón, la mayoría de las personas representan valores Unicode utilizando un sistema de codificación `8-bit` o `16-bit`. Cuando un valor Unicode es demasiado grande para caber en 8 o 16 bits, el sistema utiliza un truco especial para codificar valores más grandes. El sistema de codificación de `8-bit` se llama *UTF-8* y el sistema de `16-bit` se llama *UTF-16*.

La forma en que UTF-8 y UTF-16 son codificados es súper interesante, especialmente los trucos que usan para codificar grandes puntos de Unicode. Si te interesa ese tipo de cosas (y probablemente lo estés si sigues leyendo), definitivamente deberías ver el siguiente video. Es el mejor que he visto sobre el tema:

https://youtu.be/HhUuzFXdyNs

### Conversión UTF-16 en Dart

Aunque los puntos de código de Unicode están disponibles como `Iterable` cuando se pide `runes`, Dart utiliza UTF-16 como su codificación real para `strings` internamente. Estos valores `16-bit` se denominan unidades de código (codeUnits) en lugar de puntos (codePoints) de código.

La conversión para obtener las unidades de código UTF-16 de una cadena es fácil:

```dart
List<int> codeUnits = 'Hello😎'.codeUnits;
print(codeUnits); // [72, 101, 108, 108, 111, 55357, 56846]
```

Notas:

- Podrías estar pensando, "Hey, un `int` es 64 bytes, no 16!" Así es como se representa externamente después de hacer la conversión. El tipo `int` se supone que es genérico y no debes pensar en cuántos bytes utiliza. Internamente, strings son listas de entenros de `16-bit`.
- Pueden ver que se necesitaron dos valores UTF-16 para representar a 😎: 55357 y 56846 , que es D83D y DE0E en hex. Estos dos números se llaman *surrogate pair*, de los que sabrán todo si han visto el vídeo de arriba.

```
Decimal   Hex   Binary
--------------------------------
55357     D83D  1101100000111101 (high surrogate)
56846     DE0E  1101111000001110 (low surrogate)
String  Hex     Binary
----------------------------------------
         F60E   0000 1111 0110 0000 1110
      + 10000   0001 0000 0000 0000 0000
----------------------------------------
😎      1F60E   0001 1111 0110 0000 1110
```

Como cada unidad de código UTF-16 tiene la misma longitud, eso también hace súper fácil acceder a cualquier unidad de código por su index:

```dart
int index = 0;
int codeUnit = 'Hello😎'.codeUnitAt(index);
print(codeUnit); // 72
```

La conversión de ir de unidades de código a un String también es fácil:

```dart
List<int> codeUnits = 'Hello😎'.codeUnits;
final myString = String.fromCharCodes(codeUnits);
print(myString); // Hello😎
print(String.fromCharCode(72)); // H
```

Por muy bonito que sea, hay algunos problemas con la manipulación de strings basada en UTF-16. Por ejemplo, si estás borrando programáticamente una unidad de código a la vez, hay una buena posibilidad de que te olvides de los surrogate pairs (y grapheme clusters). Smiley ya no es tan smiley cuando eso sucede. Lee los siguientes artículos para saber más sobre este tema:

- [Trabajando con Unicode y los clusters de Grapheme en Dart](https://medium.com/flutter-community/working-with-unicode-and-grapheme-clusters-in-dart-b054faab5705)
- [Dart manipulación de cadenas hecha correctamente 👉](https://medium.com/dartlang/dart-string-manipulation-done-right-5abd0668ba3e)

### Convertir UTF-8 strings

Mientras que Dart utiliza la codificación UTF-16, Internet utiliza la codificación UTF-8. Esto significa que cuando se transfiere un texto a través de la red, es necesario convertir los strings de Dart a y desde una lista de números enteros UTF-8 encoded. De nuevo, recuerde que esto es sólo una lista de números binarios `8-bit`, específicamente un `Uint8List`. La ventaja de los valores de `8-bit` es que no tienes que preocuparte por *endianess*.

Si vieras el video que enlacé arriba y ahora entiendes cómo funciona la codificación UTF-8, podrías escribir el codificador y decodificarlo tú mismo. Sin embargo, no es necesario, porque Dart ya los tiene en su biblioteca `dart:convert`.

Importa la biblioteca así:

```dart
import 'dart:convert';
```

Entonces puedes hacer la conversión de una cadena a UTF-8 simplemente así:

```dart
Uint8List encoded = utf8.encode('Hello😎');
print(encoded); // [72, 101, 108, 108, 111, 240, 159, 152, 142]
```

Esta vez Smiley es codificado como cuatro valores de `8-bit` :

```
Decimal   Hex   Binary
------------------------
240       F0    11110000
159       9F    10011111
152       98    10011000
142       8E    10001110
String  Hex     Binary
------------------------
😎      1F60E   00001 1111 0110 0000 1110
```

Te lo digo, tienes que ver ese video. 😎

Convertir de UTF-8 a String es igual de fácil. Esta vez usa el método de decodificación:

```dart
List<int> list = [72, 101, 108, 108, 111, 240, 159, 152, 142];
String decoded = utf8.decode(list);
print(decoded); // Hello😎
```

Está bien pasar una lista de valores `int`, pero tendrás una excepción si alguno de ellos es mayor de 8 bits así que ten cuidado con eso.

## Lógica booleana

Asumo que sabes sobre los operadores `&&`, `||` y `!` de lógica booleana que usas con el tipo `bool`:

```dart
// AND
print(true && true);   // true
print(true && false);  // false
print(false && false); // false
// OR
print(true || true);   // true
print(true || false);  // true
print(false || false); // false
// NOT
print(!true);          // false
print(!false);         // true
```

Bueno, también hay equivalentes `bitwise` de estos operadores que trabajan con números binarios. Si piensas en `false` como 0 y `true` como 1, obtienes resultados similares con los operadores bitwise `&`, `|`, y `~` (más un operador adicional `^` XOR): 

```dart
// AND
print(1 & 1);  // 1
print(1 & 0);  // 0
print(0 & 0);  // 0
// OR
print(1 | 1);  // 1
print(1 | 0);  // 1
print(0 | 0);  // 0
// XOR
print(1 ^ 1);  // 0
print(1 ^ 0);  // 1
print(0 ^ 0);  // 0
// NOT
print(~1);     // -2
print(~0);     // -1
```

Bueno, los resultados son casi los mismos. El operador `~` bitwise `NOT` dio unos números extraños. Volveremos a eso en un rato, aunque probablemente ya hayas adivinado la razón.

Si quieres saber un poco más sobre las operaciones de bitwise antes de que veamos algunos ejemplos de Dart, echa un vistazo a esta serie de vídeos:

- [Bitwise Operators 1: The AND Operation](https://youtu.be/bizj3dle8Qc)
- [Bitwise Operators 2: The OR Operation](https://youtu.be/TMFnWGJEJuI)
- [Bitwise Operators 3: The XOR Operation](https://youtu.be/O9VELMn3jIY)

## Bitwise AND operador &

El operador bitwise `&` compara cada par de bits y da un 1 en el bit resultante sólo si ambos de la entrada bits fueran 1.

Aquí hay un ejemplo:

```dart
final y = 0x4a;   // 01001010
final x = 0x0f;   // 00001111
final z = x & y;  // 00001010
```

¿Por qué es eso útil? Bueno, una cosa útil es hacer un `AND` bitmask. Un *bitmask* es una forma de filtrar todo menos la información que estás buscando.

Puedes verlo en la práctica en el código fuente de Flutter `TextPainter`:

```dart
static bool _isUtf16Surrogate(int value) {
  return value & 0xF800 == 0xD800;
}
```

Este método estático comprueba un valor UTF-16 para ver si es un *surrogate pair*. Los surrogates están en el rango 0xD800-0xDFFF. El valor 0xF800 es el bitmask.

Es más fácil de entender cuando lo ves en booleano.

```
D800  1101 1000 0000 0000  (min)
DFFF  1101 1111 1111 1111  (max)
F800  1111 1000 0000 0000  (bitmask)
```

Notarán que los primeros cinco bits de cualquier sustituto son 11011. Los siguientes once bits pueden ser cualquier cosa. Así que ahí es donde entra la bitmask. Utiliza cinco bits 1 para coincidir con el patrón que está buscando y once 0 bits para ignorar el resto.

Recordemos que nuestro amigo Smiley 😎 está formado por los *surrogate pairs* D83D y DE0E, por lo que cualquiera de los dos valores debería volverse *true*. Probemos el segundo, DE0E:

```
DE0E  1101 1110 0000 1110  (test value)
F800  1111 1000 0000 0000  (bitmask)
-------------------------
D800  1101 1000 0000 0000  (result of & operation)
```

El resultado es D800, así que eso debería hacer que la comparación sea cierta, lo que significa que sí, el DE0E es un *surrogate pair*. Puedes probarlo tú mismo: 

```dart
bool _isUtf16Surrogate(int value) {
  return value & 0xF800 == 0xD800;
}

print(_isUtf16Surrogate(0xDE0E)); // true
```

## Bitwise OR operator |

El operador bitwise `|` compara cada par de bits y tiene un 1 en el bit resultante si alguno (o ambos) de los input bits fueran 1.

He aquí un ejemplo:

```dart
final y = 0x4a;   // 01001010
final x = 0x0f;   // 00001111
final z = x | y;  // 01001111
```

¿Por qué es eso útil? Bueno, también puedes usarlo como un bitmask *"turn on"* cierto bits mientras dejas otros bits sin afectar.

Por ejemplo, los colores a menudo se empaquetan en un solo entero de `32-bit`, donde los primeros 8 bits representan el valor alfa (transparencia), los siguientes 8 bits son el valor rojo, los siguientes 8 bits el valor verde, y los últimos 8 bits el valor azul.

Así que digamos que tienes este color purple semitransparente aquí:

Si quieres mantener el color pero hacerlo completamente opaco, podrías usar un OR bitmask para hacerlo. El valor hex para ese color es 802E028A:

```
alpha    red      green    blue
-----------------------------------
80       2E       02       8A
10000000 00101110 00000010 10001010
```

Completamente transparente es 0x00 y completamente opaco es 0xFF. Así que quieres hacer un bitmask que mantenga los valores de rojo, verde y azul iguales, pero que haga el alfa 0xFF. En este caso debemos esperar FF2E028A para el resultado después de que se aplique el bitmask.

```
10000000 00101110 00000010 10001010  (original)
11111111 00000000 00000000 00000000  (bitmask)
-----------------------------------
11111111 00101110 00000010 10001010  (result of | operation)
```

Como *ORing* cualquier cosa con 1 hace 1, esto convirtió todos los valores alfa en 1. *ORing* cualquier cosa con 0 no cambia el valor, así que los otros valores se mantuvieron iguales.

Inténtalo en Dart:

```dart
final original = 0x802E028A;
final bitmask = 0xFF000000;
final opaque = original | bitmask;
print(opaque.toRadixString(16)); // ff2e028a
```

¡Ya lo tengo!

Nota: Si te molesta que la cadena hex esté en *lowercase*, siempre puedes encubrirla a *uppercase*:

Nota: Si le molesta que la cadena hex esté en *lowercase*, siempre puede encubrirla a *uppercase*:

```dart
print('ff2e028a'.toUpperCase()); // FF2E028A
```

## Bitwise XOR operador ^

El operador bitwise `^` compara cada par de bits y tiene un 1 en el bit resultante si la entrada bits es diferente.

Aquí hay un ejemplo:

```dart
final y = 0x4a;   // 01001010
final x = 0x0f;   // 00001111
final z = x ^ y;  // 01000101
```

¿Por qué es eso útil? En Dart verás a menudo el operador `^` usado para crear códigos `hash`. Por ejemplo, aquí hay una clase de persona:

```dart
class Person {
  final String name;
  final int age;
  Person(this.name, this.age);

  @override
  bool operator ==(Object other) {
    return identical(this, other) ||
        other is Person &&
            runtimeType == other.runtimeType &&
            name == other.name &&
            age == other.age;
  }

  @override
  int get hashCode => name.hashCode ^ age.hashCode;
}
```

La última línea con hashCode es la parte interesante. Reproduzcamos eso aquí:


```dart
final name = 'Bob';
final age = 97;
final hashCode = name.hashCode ^ age.hashCode;

print(name.hashCode); // 124362681
print(age.hashCode);  // 97
print(hashCode);      // 124362712
print(name.hashCode.toRadixString(2).padLeft(32, '0'));
print(age.hashCode.toRadixString(2).padLeft(32, '0'));
print(hashCode.toRadixString(2).padLeft(32, '0'));
```

Aquí están los resultados binarios:

```dart
00000111011010011001111110111001  (name hash code)
00000000000000000000000001100001  (age hash code)
--------------------------------
00000111011010011001111111011000  (result of ^ operation)
```

Para los códigos hash quieres que estén lo más distribuidos posible para que no se produzcan colisiones hash. Si haces operaciones `&`, obtienes más 0s, y si haces operaciones `|`, obtienes más 1s. Como el operador `^` mantiene la distribución de 0s y 1s, es un buen candidato para crear nuevos códigos hash a partir de otros.

## El operador NOT Bitwise ~

El operador `~` invierte el valor de los bits. Antes viste estos resultados confusos:

```dart
// NOT
print(~1);     // -2
print(~0);     // -1
```

Esto tiene más sentido si miras el valor binario. Aquí está la representación `64-bit` de 1:

```
0000000000000000000000000000000000000000000000000000000000000001
```

Cuando volteas todos esos bits (es decir, el resultado de ~1), obtienes:

```
1111111111111111111111111111111111111111111111111111111111111110
```

Y si miran el video sobre *Two's Complement*, recordarán que así es como se expresa el número -2 en binario. Es una historia similar para ~0. Problema resuelto.

Por cierto, aquí hay un chiste para aquellos de ustedes que les gusta Shakespeare:

```dart
2b|~2b
```

Esa es la cuestión.

## Bit shifting << >>

Hay un último tema a tratar: bit shifting. Puedes cambiar bits a la izquierda con el operador bitwise left shift `<<` y a la derecha con el operador bitwise right shift `>>`.

Puede aprender más sobre el bit shifting en este video: 

https://youtu.be/mjqswwqE1RQ

Aquí hay un ejemplo en Dart de un shift izquierdo:

```dart
final value = 0x01;  // 00000001
print(value << 5);  // 00100000
```

Todos los bits se desplazaron a la izquierda en 5. Un valor binario de 00100000 es 32. Como dato interesante, una forma sencilla de multiplicar por dos es hacer shift left por uno:

```dart
print(7 << 1);  // 14 (decimal)
```

Aquí hay un right shift:

```dart
final value = 0x80;  // 10000000
print(value >> 3);  // 00010000
```

Esto equivale a decir que 128 right shift 3 es 16.

¿Por qué es esto importante? Bueno, un lugar donde puedes verlo en Dart es para extraer valores de un `integer` empaquetado. Por ejemplo, el código fuente de la clase `Color` tiene el siguiente getter para extraer el valor rojo de un valor de color:

```dart
/// The red channel of this color in an 8 bit value.
int get red => (0x00ff0000 & value) >> 16;
```

Esto primero utiliza un `AND` bitmask para encontrar sólo la parte *red* del valor ARGB (alpha red green blue) y luego mueve el resultado al principio. Así es como se vería con nuestro 802E028A color purple de antes.

```
alpha    red      green    blue
-----------------------------------
80       2E       02       8A
10000000 00101110 00000010 10001010  (original)
00000000 11111111 00000000 00000000  (bitmask)
-----------------------------------
00000000 00101110 00000000 00000000  (result of &)
-----------------------------------
00000000 00000000 00000000 00101110  (result of >> 16)
```

Y aquí está en Dart:

```dart
final purple =  0x802E028A;
final redBitmask = 0x00FF0000;
final masked = purple & redBitmask;
final redPart = masked >> 16;
print(redPart.toRadixString(16)); // 2e
```

## Puntos clave

¡Lo lograste! Aquí están los puntos clave de este artículo:

- Los datos binarios pueden ser expresados como una lista de números enteros.
- `Uint8List` es una lista de `8-bit` de números enteros sin signo  y es más eficiente que `List<int>` cuando se trabaja con grandes cantidades de datos binarios.
- Se puede utilizar `BytesBuilder` para añadir datos binarios a una lista.
- Los datos de los bytes están respaldados por un `BytesBuffer` y se pueden obtener diferentes vistas de los bytes subyacentes. Por ejemplo, se pueden ver los bytes en los segmentos `8-bit`, `16-bit`, `32-bit`, o `64-bit`, ya sea como enteros son o sin signo.
- Con los trozos mayores de 8 bits hay que tener en cuenta *endianness*, que se ve afectado por la máquina o el formato de almacenamiento subyacente. Se puede utilizar `ByteData` para especificar ya sea vistas *big endian* o *little endian*.
- Prefije un número con `0x` para escribirlo en notación hexadecimal.
- Dart strings son listas de valores UTF-16 y se conocen como unidades de código.
- Convierte strings a UTF-8 utilizando la biblioteca `dart:convert`.
- Los operadores lógicos bitwise son `&`, `|`, `^`, y `~`, y los operadores shift son `<<` y `>>`.
- Los operadores y las máscaras de bits permiten acceder y manipular los bits individuales.

## Continuando

Un par de áreas que merecen un estudio más profundo son los streams de bytes y cómo transformar los datos de esos streams. Por ejemplo, se puede obtener un byte stream al leer o descargar un archivo. En lugar de esperar a que llegue todo el archivo antes de trabajar con él, puedes empezar a transformar los valores bytes a UTF-8 (o algo más) a medida que los obtengas.

Recuerda dejar un comentario o hacer una pregunta si algo no está claro.