Tipo de sistema Kotlin

Sistema de tipo Kotlin

Este artículo echa un vistazo al sistema de tipos en Kotlin, que implica un concepto muy importante que se dice a menudo sobre la nulabilidad y por qué  Kotlin puede reducir la excepción de puntero nulo hasta cierto punto en comparación con Java . Además, Kotlin adopta completamente diferentes ideas de Java para definir su sistema de tipos. Es precisamente porque este sistema de tipos tiene naturalmente la ventaja de que la frecuencia de excepciones de puntero nulo de Kotlin es significativamente menor que la de Java. Además, Kotlin considera el uso de un sistema de tipo completamente diferente al de Java, y cómo logra una gran compatibilidad e interoperabilidad.

1. Resolver el concepto

Antes de aprender el sistema de tipos de Kotlin, deberíamos pensar en los siguientes conceptos juntos: si estos conceptos no están claros, es difícil entender fundamentalmente el sistema de tipos de Kotlin y por qué Kotlin es superior a Java en el sistema de tipos.

1.1 La naturaleza de los tipos

¿Cuál es la naturaleza de los tipos? ¿Por qué las variables tienen tipos? Estas dos preguntas están bien respondidas en Wikipedia:

El tipo es en realidad la clasificación de los datos, que determina los posibles valores del tipo y las operaciones que se pueden completar sobre el valor del tipo.  Es necesario prestar especial atención a la siguiente explicación:  "Los posibles valores de este tipo y las operaciones que se pueden realizar sobre el valor de este tipo".  Debido a que el sistema de tipos de Java no se ajusta realmente a esta regla al 100%, esto es también la existencia del sistema de tipos Java. A continuación se hará un análisis específico.

1.2 Clase y tipo

En cuanto a  clase  y ** tipo **, se estima que muchos desarrolladores tienden a ignorar la diferencia entre ellos, porque no se distinguen tan finamente en escenarios de aplicaciones reales. Tendemos a equiparar la clase con el tipo cuando la usamos, de hecho, son completamente diferentes. De hecho, también se refleja en Java, como  List<String>、Lis<Integer> y  List. Para los primeros  List<String> y  List<Integer> únicos tipos, List no se puede decir que sea una clase, pero  puede ser una clase List o un tipo (un tipo primitivo en Java) .

De hecho, en Kotlin, este concepto se eleva a un nivel superior, porque cada clase en Kotlin tiene un tipo más anulable. Por ejemplo, una Stringclase corresponde a dos  String tipos y  String? un tipo anulable. En Java, a excepción de los tipos genéricos, cada clase corresponde a un solo tipo (la clase en sí), por lo que a menudo se ignora.

Podemos dividir las clases en Kotlin en dos categorías (Java también se puede dividir de esta manera):  clases genéricas y clases no genéricas .

Clase no genérica

Primero, las clases no genéricas son las clases generales a las que están más expuestas en el desarrollo.Cuando una clase general define una variable, su clase es en realidad el tipo de la variable. P.ej:

var msg: String Aquí podemos decir que Stringla clase  y el  tipo demsg  variable son iguales. Pero hay un tipo especial en Kotlin que es el tipo anulable , que se puede definir como el tipo de clase y la variable aquí es diferente. Entonces, una clase en Kotlin generalmente corresponde a al menos dos tipos, por lo que clase y tipo no son lo mismo.var msg: String?StringmsgString?

Clase genérica

Las clases genéricas son más complejas que las no genéricas, de hecho, una clase genérica puede corresponder a un número infinito de tipos . ¿Por qué dices que es realmente fácil de entender? Sabemos por el artículo anterior que al definir una clase genérica, se definen parámetros genéricos. Si desea obtener un tipo genérico válido, debe pasar argumentos de tipo específico en el lugar de uso externo para reemplazar los parámetros de tipo en la definición. . Sabemos List que es una clase en Kotlin  , no es un tipo. Se puede derivar en una variedad infinita de tipos genéricos, por ejemplo List<String>、List<Int>、List<List<String>>、List<Map<String,Int>>.

1.3 Subclases, subtipos y superclases, supertipos

Generalmente decimos que una subclase es una clase derivada, y esta clase generalmente hereda su superclase . Por ejemplo:  class Student: Person()aquí Studentgeneralmente se denomina Personsubclase, que  Persones Studentuna superclase.

Las definiciones de subtipos y supertipos son completamente diferentes.De la distinción entre clases y tipos anterior, sabemos que una clase puede tener muchos tipos, por lo que los subtipos no solo son tan estrictos como la relación de herencia de las subclases.

La regla de definición de subtipo es generalmente así: en  cualquier momento, si necesita cualquier lugar del valor del tipo A, puede usar el valor del tipo B para reemplazarlo, entonces puede decir que el tipo B es un subtipo del tipo A o llamado el tipo A B es un tipo de supertipo . Puede verse claramente que las reglas para los subtipos son más relajadas que las de los subtipos. Entonces podemos analizar los siguientes ejemplos juntos:

descripción de la imagen

Sugerencias : Cierto tipo también es un subtipo de sí mismo. Obviamente, cuando el valor del tipo Persona aparece en cualquier lugar, Persona definitivamente puede ser reemplazado. Lo que pertenece a la relación de subtipo es generalmente también una relación de subtipo. Los valores de tipo String no pueden reemplazar el lugar donde aparecen los valores de tipo Int, por lo que no existe una relación de subtipo entre ellos.

Veamos un ejemplo, estar vacío de todos los tipos de clase no vacía correspondiente al tipo de clase es un subtipo, pero no por el contrario , es Personun tipo no vacío que es un Person?subtipo de tipo anulable, Bueno, obviamente, cualquier Person?disponible Cuando aparece un valor de tipo vacío, se puede Personreemplazar con un valor de tipo no vacío.

De hecho, puedo experimentarlos en el proceso de desarrollo. Por ejemplo, los estudiantes cuidadosos encontrarán que en el proceso de desarrollo de Kotlin, si una función recibe un parámetro de un tipo que acepta valores NULL, se pasa un tipo no nulo en lugar de la llamada Los parámetros reales son legales. Pero si una función recibe un parámetro de un tipo que no es nulo, el compilador le preguntará si pasa un argumento de un tipo que admite valores nulos. Puede haber un problema de puntero nulo y necesita hacer un juicio no nulo.  Porque sabemos que los tipos que no son nulos son más seguros que los tipos que aceptan valores NULL. Para entender la imagen:

descripción de la imagen

2. La razón esencial de la existencia de NPE en el sistema de tipos de Java

Con la descripción anterior de la esencia de los tipos, echemos un vistazo a algunos tipos básicos en Java para aplicar la definición de la esencia de los tipos y ver qué está mal.

Utilice la definición del inttipo para verificar el tipo

Por ejemplo, una int variable de un  tipo indica que solo puede almacenar datos del  int tipo. Todos sabemos que usa 4 bytes para almacenar, y el rango de valores es -2147483648 ~ 2147483647. Luego, especifique los valores posibles del tipo , y luego podemos El valor del tipo realiza operaciones aritméticas. Parece que no hay problema, el inttipo y la esencia del tipo encajan perfectamente.

Pero, String¿qué pasa con el tipo? ¿Es lo mismo? Por favor siga leyendo:

Utilice la definición del tipo para verificar el Stringtipo o el tipo correspondiente a la otra clase definida

Por ejemplo, una String variable de un  tipo puede tener dos valores en Java: uno es Stringuna instancia de la clase y el otro es null. Entonces podemos ser de algún valor para estas operaciones, la primera Stringinstancia de un curso de clase le permite llamar a Stringla clase de todos los métodos de operación, pero para el segundo nullvalor, la operación es muy limitada, si se ve obligado a usar nullpara operar value Stringpara operar una clase, luego Felicitaciones, obtendrá una NullPointerExceptionexcepción de puntero nulo.

Para la solidez del programa en Java, esto requiere que los desarrolladores String hagan juicios adicionales sobre el valor del  tipo y luego realicen el procesamiento correspondiente; si no se realiza ningún procesamiento de juicio adicional, es fácil obtener una excepción de puntero nulo.

Esto significa que hay varios valores para el mismo tipo de variable, pero no se pueden tratar por igual. La comparación de los int valores existentes de los tipos anteriores  se tratan de forma coherente, y todos los valores posibles de este tipo se pueden someter a la misma operación. Veamos un ejemplo muy interesante:

descripción de la imagen

Parece que incluso el valor en Java instanceofno se reconoce nullcomo un Stringtipo. Las operaciones de estos dos valores también son completamente diferentes: el real Stringte permite llamar a cualquiera de sus métodos, mientras que el nullvalor solo permite operaciones muy limitadas . Entonces, ¿cómo resuelve el sistema de tipos de Kotlin tales problemas? Continúe mirando hacia abajo.

3. Cómo resuelve el problema el sistema de tipos de Kotlin

Los tipos en el sistema de tipos de Java Stringu otras clases personalizadas parecen ser inconsistentes con la definición esencial del tipo, pero todos los valores posibles del tipo se tratan de manera diferente, lo cual es ambiguo. Se necesitan juicios adicionales. El problema inmediato es que supone una carga adicional para los desarrolladores hacer juicios no nulos. Una vez que no se manejan correctamente, aparecerá un puntero nulo y hará que el programa se bloquee. Esta es la esencia del problema del puntero nulo en Java.

Al comprender la esencia del problema, Kotlin ha hecho un gran movimiento que consiste en dividir todos los tipos en Kotlin en dos tipos: uno es un tipo no vacío, el otro es un tipo que acepta valores NULL; entre ellos, no es vacío. las variables no permiten nulloperaciones de asignación de valor. En otras palabras, Stringlos tipos no vacíos solo existen Stringpara instancias de la clase y no hay nullvalores, por lo Stringque puede usar Stringtodos los métodos relacionados de la clase para valores de tipos no vacíos , y no no hay ambigüedad .

Por supuesto, también hay casos de nulo, por lo que puede usar el tipo que acepta valores NULL. Cuando se usa la variable de tipo que admite valores NULL, el compilador hará ciertos juicios para el tipo que admite valores NULL en tiempo de compilación. class El método le indica que necesita realizar un procesamiento nulo adicional. En este momento, el desarrollador realizará el procesamiento nulo de acuerdo con las indicaciones. Imagine que el procesamiento es así. ¿Su código de Kotlin seguirá teniendo punteros nulos? (Pero hay muy importante es definir una variable y debe dejar en claro si es anulable o no anulable. Si define un tipo anulable, debe ser responsable de él, y el compilador también le pedirá ayuda. hacer un proceso de anulación adicional para ello). Echemos un vistazo a algunos ejemplos:

  • Las variables o constantes de tipo no nulo no pueden recibir valores nulos

descripción de la imagen

  • En una variable o constante de tipo no vacíois(相当于java中instanceof)

descripción de la imagen

  • La operación directa de la variable o constante del tipo que acepta valores NULL provocará errores de compilación obvios y provocará la operación nula.

Sin embargo, lo anterior son todas las cosas que Java no puede brindarle, por lo que generalmente hay tres estados en los programas de Java: un tipo de budismo está anulado y, a menudo, hay problemas de puntero nulo. La otra es juzgar todos los espacios en blanco en un cerebro, pero el código está lleno de if-elsecódigos y la legibilidad es muy pobre.

La última es que los desarrolladores que están muy familiarizados con la lógica del programa y el flujo de datos normalmente pueden juzgar dónde se necesita el procesamiento nulo y dónde no, lo que es extremadamente exigente para los desarrolladores, porque la gente siempre comete errores.

4. Tipo que acepta valores NULL

4.1 Operador de llamada segura "?."

?. Equivalente al procesamiento nulo, si no es nulo, ejecute la expresión detrás de?., De lo contrario devuelva nulo

text?.substring(0,2) //相当于 if(text != null) text.substring(0,2) else null

De hecho, la orden de Kotlin para juzgar el tipo de tratamiento puede estar vacía de muchos problemas, todos sabemos que el tratamiento no es más que una oración vacía para hacer en Java if-elseo un ? xxx : xxxoperador ternario para lograr.

Pero a veces todo el código es una "flecha" cuando se considera que el anidamiento está vacío y la legibilidad es muy pobre. De los ejemplos anteriores se puede ver ?.que if-elseguardar una gran cantidad de código, que no puede revelar completamente sus ventajas, el siguiente ejemplo es aún más obvio.

Procesamiento anidado if-else en Java

descripción de la imagen

Operador de llamadas seguras en Kotlin ?. manejo de llamadas en cadena

descripción de la imagen

Comparando la implementación de los dos métodos, ¿cree que Kotlin puede ser más adecuado para usted? Utilice el método de llamada en cadena?. Para desatar el procesamiento if-else anidado.

4.2 Operador de Elvis  "?:"

Si?: La expresión anterior es nula, ejecute la expresión después de?:, Generalmente se usará junto con? .. (Nota: no es lo mismo que el operador ternario? Xxx: xxx en Java)

descripción de la imagen

4.3 Operador de conversión de tipo seguro  como?

Si la conversión de tipo falla, se devuelve el valor nulo; de lo contrario, se devuelve el valor correcto después de la conversión de tipo

val student = person as? Student//相当于 if(person is Student) person as Student else null

4.4 ¡ Operador de aserción no vacío  !  Y  contrato

El operador de aserción no nula !!, es forzar al compilador a decirle al compilador que el valor de esta variable no puede ser nulo, y existe riesgo de uso. Una vez que la existencia sea nula, se lanzará directamente una excepción de puntero nulo .

Muchos desarrolladores de Kotlin odian este operador y sienten que no es elegante de escribir y afecta la legibilidad del código Cómo evitar el uso del  operador !! en el código de Kotlin  .

De hecho, existen escenarios de uso para aserciones no vacías. Por ejemplo, ya ha juzgado una variable en una función, pero la usa de nuevo en la lógica posterior y puede estar seguro de que no puede estar vacía. Se puede compilar En este momento, el dispositivo no puede identificar si no está vacío, pero debido a que es un tipo que acepta valores NULL, le pedirá que realice un procesamiento nulo, lo cual es molesto, ¿no es así? ¡Muchas personas pueden haberlo adoptado en este momento  ! De  hecho, carece de legibilidad.

En respuesta a los problemas anteriores, además de las soluciones dadas en el artículo anterior, esta vez proporcionamos una nueva solución, que es un contrato (en realidad, le dice activamente al compilador una cierta regla para que no solicite un procesamiento nulo )  El contrato propuso oficialmente que es la versión de Kotlin1.3. Aunque todavía está en Experimental (como un contrato personalizado), de hecho, el código interno de Kotlin ha utilizado el contrato durante mucho tiempo. Echemos un vistazo a cómo el contrato integrado resuelve este problema.

descripción de la imagen

Echemos un vistazo al código fuente de implementación interna del contrato integrado:

descripción de la imagen

4.5 Tipos de plataforma compatibles con Java

Por lo anterior, podemos saber que Kotlin tiene un sistema de tipos completamente diferente al de Java. No existen los denominados tipos que aceptan valores NULL ni tipos que no aceptan valores NULL en Java. Pero todos sabemos que Kotlin y Java son muy interoperables y son casi completamente compatibles con Java. Entonces, ¿cómo es Kotlin compatible con los tipos de variables en Java?

Definitivamente necesitamos llamar al código Java con frecuencia en Kotlin, y algunas personas pueden responder que las @NotNull和@Nullableanotaciones se usan para identificarlas en Java . De hecho Kotlin puede identificar una variedad de diferentes estilos de notas, incluyendo  javax.annotation, android.support.annotation, org.jetbrains.annotationy así sucesivamente. Sin embargo, algunas bibliotecas de terceros anteriores no escribieron tal especificación, y es obvio que este problema no se puede resolver por completo de esta manera.

Entonces, Kotlin introdujo un nuevo concepto llamado:  tipo de plataforma. El tipo de plataforma es esencialmente un tipo que Kotlin no conoce acerca de la información de nulabilidad. Puede tratarse como un tipo que admite nulos o un tipo que no admite nulos . Esto significa asumir la responsabilidad total de las operaciones realizadas en este tipo como en el código Java.

Entonces, para los parámetros de función en Java, cuando Kotlin llama, el sistema manejará los tipos que aceptan valores NULL de forma predeterminada (por razones de seguridad). Si está claro que no es nulo, puede modificarlo directamente a un tipo no nulo, y el sistema no reportará errores de compilación Sí, pero una vez procesado de esta manera, se debe garantizar que no puede estar vacío.

descripción de la imagen

Entonces, el problema está llegando. Mucha gente se pregunta ¿por qué no convertir todos los tipos que aceptan valores NULL directamente por razones de seguridad? De hecho, este esquema parece factible, pero de hecho es un poco incorrecto. Para algunas variables que son claramente imposibles de ser anulables, es Es necesario realizar una gran cantidad de operaciones nulas adicionales. De lo contrario, el tipo no vacío no tiene ningún significado.

5. Tipos de datos básicos y otros tipos básicos

5.1 Tipos de datos básicos

Todos sabemos que se hace una distinción entre los tipos de datos básicos y los tipos de paquetes en Java. Por ejemplo, una intvariable de un tipo de datos básico almacena directamente su valor. Una Stringvariable de tipo de referencia (tipo contenedor) solo almacena una referencia  a la dirección de memoria del objeto. Los tipos de datos básicos tienen las ventajas de un almacenamiento y transmisión naturales eficientes, pero no se pueden llamar directamente y no se pueden usar como tipos de argumentos genéricos en colecciones en Java.

De hecho , Kotlin no se divide en tipos de datos básicos y tipos de paquetes como Java, siempre es del mismo tipo en Kotlin . Mucha gente probablemente preguntará, dado que los tipos de datos básicos y los tipos de empaquetado en Kotlin son los mismos, ¿significa que Kotlin usa tipos de referencia para almacenar datos? ¿Es muy ineficiente? Este no es el caso. Kotlin intentará convertir los Inttipos equivalentes en los inttipos de datos básicos en Java en tiempo de ejecución , y cuando encuentre colecciones o genéricos similares, se convertirá en los Integertipos de paquetes equivalentes correspondientes en Java .

descripción de la imagen

5.2 ¿Cualquiera y Cualquiera?

Cualquier tipo es el súper tipo de todos los tipos no nulos, y Any? Type es el súper tipo de todos los tipos, es decir, el súper tipo de los tipos no nulos es también el súper tipo de todos los tipos que aceptan valores NULL. Porque Any? Es un supertipo de Any . El nivel específico puede referirse a la siguiente imagen:

descripción de la imagen

5.3 Tipo de unidad

El tipo de unidad también es el tipo vacío en Kotlin, que es equivalente al tipo vacío en Java. Se puede omitir de forma predeterminada

5.4 Nada de tipo

El tipo Nothing es un subtipo de todos los tipos. Es tanto un subtipo de todos los tipos que no aceptan valores NULL como un subtipo de todos los tipos que aceptan valores NULL, porque Nothing es un subtipo de Nothing ?, pero Nothing? Es un subtipo de todos los tipos que aceptan valores NULL.  Específicamente, puede ver el siguiente diagrama de estructura jerárquica:

descripción de la imagen

6. Colección y tipos de matriz

6.1 La diferencia y la conexión entre una colección de variables y una colección de solo lectura (tome la colección Collection como ejemplo)

La diferencia entre la colección de colección de solo lectura y la colección de variables MutableCollectio:

  • En Collection, solo hay métodos para acceder a elementos, no métodos como agregar, eliminar y borrar. En MutableCollection, hay más métodos para modificar elementos que en Collection.

  • Las colecciones de colección de solo lectura están conectadas con las colecciones mutables de MutableCollectio:

  • MutableCollection es en realidad una subinterfaz de la interfaz de colección Collection y existe una relación de herencia entre ellas.

descripción de la imagen

6.2 La relación entre clases

A través del archivo Collection.kt, puede saber que existen estas colecciones Iterable (iterador de solo lectura) y MutableIterable (iterador variable), Collection y MutableCollection, List y MutableList, Set y MutableSet, Map y MutableMap. Entonces, ¿cuál es el diagrama de clases entre ellos?

Las interfaces Iterable y MutableIterable son las interfaces principales de las colecciones de solo lectura y mutable respectivamente. La colección hereda Iterable y las interfaces List y Set heredan de Collection. La interfaz Map es más especial y es una interfaz separada, y la interfaz MutableMap se hereda del mapa.

descripción de la imagen

 

6.3 Correspondencia entre colecciones en Java y colecciones en Kotlin

Acabamos de decir que el diseño de colecciones en Kotlin es diferente al de Java, pero cada interfaz de Kotlin es una instancia de su interfaz de colección Java correspondiente, es decir, existe una cierta correspondencia entre las colecciones en Kotlin y las colecciones en Kotlin. Las clases ArrayList y HashSet en Java son en realidad las clases de implementación de las interfaces de colección MutableList y MutableSet en Kotlin. Agregue esta relación, el diagrama de relación de clases anterior se puede mejorar aún más.

descripción de la imagen

6.4 Inicialización de la colección

Dado que las colecciones se dividen principalmente en colecciones de solo lectura y colecciones de variables en Kotlin, las funciones para inicializar colecciones de solo lectura y colecciones de variables son diferentes. Lista establecida en, por ejemplo, para una colección de solo lectura, inicialice el método ListOf () comúnmente usado , para un conjunto de inicialización de variables comúnmente usado mutableListOf () o cree directamente ArrayList <E> .

Debido a que la implementación interna de mutableListOf () todavía usa la creación de ArrayList, esta ArrayList es en realidad java.util.ArrayList <E> en Java, pero typealias se usa en Kotlin (el uso de typealias se ha introducido en detalle antes). Solamente. Para obtener más detalles, consulte la implementación de esta clase kotlin.collections.TypeAliasesKt .

6.5 Precauciones para el uso de colecciones

  • Las colecciones de solo lectura se prefieren en todas partes del código, y las colecciones mutables se utilizan solo cuando es necesario modificar la colección.

  • Las colecciones de solo lectura no son necesariamente inmutables. Acerca de esto de solo lectura e inmutable es similar al principio de solo lectura e inmutable de val.

  • No puede pasar una colección de tipos de solo lectura como parámetro a una función con una colección de tipos de variables.

6.6 Reglas de conversión colectiva para tipos de plataforma

Al igual que el tipo de plataforma de nulabilidad mencionado anteriormente, no hay forma de saber el tipo de información de nulabilidad en Kotlin. Puede tratarse como un tipo que admite nulos o un tipo que no admite nulos. El tipo de plataforma de la colección es similar a esto, y las variables del tipo de colección declaradas en Java también se consideran como el tipo de plataforma . Una colección de tipo plataforma es esencialmente una colección con mutabilidad desconocida. Kotlin puede considerarla como una colección de solo lectura o una colección mutable.  De hecho, esto no es muy importante, porque solo necesita elegir según sus necesidades. Es decir, todas las operaciones que desea realizar pueden funcionar normalmente, a diferencia de la plataforma de nulabilidad, existen operaciones de juicio adicionales y el riesgo de punteros nulos.

Sugerencias : pero cuando decida qué tipo de Kotlin usar para representar una variable de tipo de colección en Java, debe considerar las siguientes tres situaciones:

  • 1.¿Está la colección vacía?

Si está vacío, se convertirá en la colección de Kotlin y agregará  ?, Por ejemplo, en Java, se List<String>convertirá en Kotlin'sList<String>?

  • 2. ¿Están vacíos los elementos del conjunto?

Si está vacío, se convertirá en una colección de argumentos genéricos en Kotlin y agregará  ?, Por ejemplo, en Java se List<String>convertirá a Kotlin enList<String?>

  • 3. ¿El método de operación modificará la colección? (La colección es de solo lectura o variable)

Si es de solo lectura, por ejemplo, el de Java se List<String>convierte a Kotlin List<String>; si es variable, por ejemplo, el de Java se List<String>convierte a Kotlin MutableList<String>.

** Consejos: ** Por supuesto, una o más de las tres situaciones anteriores pueden ocurrir al mismo tiempo, por lo que el tipo de colección en Kotlin es también el tipo de reorganización final en múltiples situaciones.

7. Resumen

Hasta ahora, el sistema de tipos de Kotlin es básicamente el mismo, y el contenido involucrado está básicamente involucrado. De hecho, comprenda detenidamente por qué el sistema de tipos de Kotlin está diseñado de esta manera. Realmente tiene sentido. A menudo escuchamos a la gente elogiar las ventajas de Kotlin sobre Java. Mucha gente dice que hay muchas excepciones de puntero nulo, pero ¿por qué Kotlin tiene menos excepciones de puntero nulo que Java? Creo que este artículo es suficiente para responderte.

 

Supongo que te gusta

Origin blog.csdn.net/PrisonJoker/article/details/114241357
Recomendado
Clasificación