Go Basics 18: comprender la naturaleza de los métodos para elegir el tipo de receptor correcto

Aunque el lenguaje Go no admite elementos de sintaxis clásicos orientados a objetos, como clases, objetos, herencia, etc., el lenguaje Go también tiene métodos. En comparación con las funciones, los métodos en el lenguaje Go solo tienen un parámetro más en el formulario de declaración, al que Go llama parámetro del receptor. El parámetro del receptor es el vínculo entre el método y el tipo.

La forma de declaración general de un método Go es la siguiente:

func (receiver T/*T) MethodName(参数列表) (返回值列表) {
	// 方法体
}

T en la declaración del método anterior se denomina tipo base de receptor. A través del receptor, el método anterior está obligado a escribir T. En otras palabras, el método anterior es un método de tipo T, que podemos llamar a través de una instancia de tipo T o *T, como se muestra en el pseudocódigo a continuación:

var t T
t.MethodName(参数列表)
var pt *T = &t
pt.MethodName(参数列表)

Los métodos Go tienen las siguientes características.

1) Si la primera letra del nombre del método está en mayúscula determina si el método es un método exportado.
2) Las definiciones de métodos deben colocarse en el mismo paquete que las definiciones de tipos. De esto podemos deducir: los métodos no se pueden agregar a tipos nativos (como int, float64, map, etc.), pero los métodos solo se pueden definir para tipos personalizados (el código de muestra es el siguiente).

// 错误的做法
func (i int) String() string {
    
     // 编译器错误:cannot define new methods on non- local type int
	return fmt.Sprintf("%d", i)
}
// 正确的做法
type MyInt int
func (i MyInt) String() string {
    
    
	return fmt.Sprintf("%d", int(i))
}

De la misma manera, se puede deducir que no se pueden definir métodos en paquetes Go para tipos personalizados en otros paquetes.

3) Cada método solo puede tener un parámetro de receptor y no se admiten múltiples listas de parámetros de receptor o parámetros de receptor de longitud variable. Un método solo se puede vincular a un tipo base y el lenguaje Go no admite métodos que vinculen varios tipos al mismo tiempo.

4) El tipo base del parámetro del receptor en sí no puede ser un tipo de puntero o un tipo de interfaz. El siguiente ejemplo muestra esto:

type MyInt *int
func (r MyInt) String() string {
    
     // 编译器错误:invalid receiver type MyInt (MyInt is a pointer type)
	return fmt.Sprintf("%d", *(*int)(r))
}
type MyReader io.Reader
func (r MyReader) Read(p []byte) (int, error) {
    
     // 编译器错误:invalid receiver type MyReader (MyReader is an interface type)
	return r.Read(p)
}

En comparación con otros lenguajes de programación convencionales, el lenguaje Go solo tiene un receptor más de función a método, lo que reduce en gran medida el umbral para que Gopher aprenda métodos. Aun así, Gopher todavía tiene confusión a la hora de captar la esencia del método y elegir el tipo de receptor, este artículo se centrará en estas confusiones.

la naturaleza del método

Como se mencionó anteriormente, el lenguaje Go no tiene clases y los métodos y tipos están conectados a través de receptores. Podemos definir métodos para cualquier tipo nativo no integrado, como el siguiente tipo T:

type T struct {
    
    
	a int
}
func (t T) Get() int {
    
    
	return t.a
}
func (t *T) Set(a int) int {
    
    
	t.a = a
	return t.a
}

Cuando un objeto C++ llama a un método, el compilador pasará automáticamente este puntero que apunta al objeto mismo como el primer parámetro del método. Para Go, lo mismo ocurre con el receptor: pasamos el receptor como primer parámetro a la lista de parámetros del método.

El método de tipo T en el ejemplo anterior se puede convertir de manera equivalente en la siguiente función ordinaria:

func Get(t T) int {
    
    
	return t.a
}
func Set(t *T, a int) int {
    
    
t.a = a
	return t.a
}

Esta función convertida es el prototipo del método. Es solo que en el lenguaje Go, el compilador Go completa automáticamente esta conversión equivalente al compilar y generar código. Se proporciona un nuevo concepto en la especificación del lenguaje Go, que nos permite comprender más completamente la conversión equivalente anterior.
El uso general de los métodos Go es el siguiente:

var t T
t.Get()
t.Set(1)

Podemos reemplazar la llamada al método anterior con el siguiente método equivalente:

var t T
T.Get(t)
(*T).Set(&t, 1)

Esta expresión de llamar directamente a un método con el nombre de tipo T se denomina expresión de método (Expresión de método). El tipo T solo puede llamar a métodos en el conjunto de métodos de T. De manera similar, T solo puede llamar a métodos en el conjunto de métodos de T.

Esta forma de llamar a un método a través de una expresión de método es exactamente la misma que la conversión equivalente de método a función que hicimos antes. Esto es lo que es un método Go: una función ordinaria que toma como primer argumento una instancia del tipo al que está vinculado el método.

El tipo del método Go en sí es una función ordinaria, e incluso podemos asignarlo como un valor a una variable de tipo función:

var t T
f1 := (*T).Set // f1的类型,也是T类型Set方法的原型:func (t *T, int)int
f2 := T.Get // f2的类型,也是T类型Get方法的原型:func (t T)int
f1(&t, 3)
fmt.Println(f2(t))

Elija el tipo de receptor correcto

Con el análisis anterior de la esencia de los métodos Go, es mucho más sencillo comprender el receptor y elegir el tipo de receptor correcto al definir los métodos. Echemos un vistazo a las fórmulas de transformación equivalentes para métodos y funciones:

func (t T) M1() <=> M1(t T)
func (t *T) M2() <=> M2(t *T)

Vemos que el tipo de parámetro del receptor del método M1 es T y el tipo de parámetro del receptor del método M2 es *T.

1) Cuando el tipo de parámetro del receptor es T, seleccione el receptor del tipo de valor

Cuando se selecciona T como tipo de parámetro del receptor, el método M1 de T es equivalente a M1(t T). Los parámetros de la función Go se pasan por copia de valor, lo que significa que t en el cuerpo de la función M1 es una copia de la instancia de tipo T. De esta manera, cualquier modificación al parámetro t en la implementación de la función M1 solo afectará la copia, no la copia. Afecta la instancia de tipo T original.

2) Cuando el tipo de parámetro del receptor es *T, seleccione el receptor de tipo puntero.

Cuando se selecciona *T como tipo de parámetro del receptor, el método M2 de T es equivalente a M2(t *T). La t que pasamos a la función M2 es la dirección de la instancia de tipo T, de modo que cualquier modificación al parámetro t en el cuerpo de la función M2 se reflejará en la instancia de tipo T original.

El siguiente ejemplo demuestra el impacto de seleccionar diferentes tipos de receptores en instancias del tipo original:

type T struct {
    
    
	a int
}
func (t T) M1() {
    
    
	t.a = 10
}
func (t *T) M2() {
    
    
	t.a = 11
}
func main() {
    
    
	var t T // t.a = 0
	println(t.a)
	t.M1()
	println(t.a)
	t.M2()
	println(t.a)
}

Ejecute este programa:

0
0
11

En este ejemplo, el campo a se modifica en los cuerpos del método M1 y M2, pero M1 (usando el receptor de tipo de valor) solo modifica una copia de la instancia y no tiene ningún efecto en la instancia original. Por lo tanto, después de llamar a M1, el El valor de la salida ta sigue siendo 0. M2 (usando un receptor de tipo puntero) modifica la instancia en sí, por lo que después de llamar a M2, el valor de ta se convierte en 11.

Muchos principiantes de Go todavía tienen esta duda: ¿Pueden las instancias de tipo T solo llamar a métodos cuyo receptor sea de tipo T, pero no pueden llamar a métodos cuyo receptor sea de tipo *T ? la respuesta es negativa. Ya sea una instancia de tipo T o una instancia de tipo T, puede llamar al método del receptor que es de tipo T, o puede llamar al método del receptor que es de tipo T.

El siguiente ejemplo demuestra esto:

package main

type T struct {
    
    
	a int
}

func (t T) M1() {
    
    

}
func (t *T) M2() {
    
    
	t.a = 11
}
func main() {
    
    
	var t T
	t.M1() // ok
	t.M2() // <=> (&t).M2()
	var pt = &T{
    
    }
	pt.M1() // <=> (*pt).M1()
	pt.M2() // ok
}

Vemos que está bien que una instancia de tipo T llame al método M2 con un tipo de receptor de T. De manera similar, también está bien que una instancia de tipo T pt llame al método M1 con un tipo de receptor de T. De hecho, todo esto es azúcar sintáctico de Go. El compilador de Go lo convierte automáticamente al compilar y generar código.

Llegados a este punto, podemos sacar conclusiones preliminares sobre la selección del tipo de receptor .

● Si desea modificar la instancia del tipo, seleccione el tipo *T para el receptor.

● Si no es necesario modificar la instancia de tipo, puede elegir el tipo T o el tipo *T para el receptor, pero considerando que al llamar al método Go, el receptor se pasa al método en forma de copia de valor 如果类型的size较大,以值形式传入会导致较大损耗,这时选择*T作为receiver类型会更好些.

Resuelva inteligentemente problemas difíciles basándose en la comprensión de la esencia de los métodos Go.

package main

import (
	"fmt"
	"time"
)

type field struct {
    
    
	name string
}

func (p *field) print() {
    
    
	fmt.Println(p.name)
}

func main() {
    
    

	data1 := []*field{
    
    {
    
    "one"}, {
    
    "two"}, {
    
    "three"}}

	for _, v := range data1 {
    
    
		go v.print()
	}
	data2 := []field{
    
    {
    
    "four"}, {
    
    "five"}, {
    
    "six"}}

	for _, v := range data2 {
    
    
		go v.print()
	}
	time.Sleep(3 * time.Second)
}

Los resultados de ejecución son los siguientes (los resultados pueden ser diferentes debido a diferentes órdenes de programación de rutinas):

one
two
three
six
six
six

¿Por qué el resultado de salida de la iteración 3 de data2 es "seis" en lugar de "cuatro", "cinco" y "seis"?

Bien, analicémoslo. Primero, de acuerdo con la esencia del método Go: una función ordinaria con la instancia del tipo vinculado al método como primer parámetro, realice una transformación equivalente a este programa (aquí usamos expresiones de método), el código fuente transformado es como sigue:

type field struct {
    
    
name string
}
func (p *field) print() {
    
    
fmt.Println(p.name)
}
func main() {
    
    
data1 := []*field{
    
    {
    
    "one"}, {
    
    "two"}, {
    
    "three"}}
for _, v := range data1 {
    
    
go (*field).print(v)
}
data2 := []field{
    
    {
    
    "four"}, {
    
    "five"}, {
    
    "six"}}
for _, v := range data2 {
    
    
go (*field).print(&v)
}
time.Sleep(3 * time.Second)
}

Aquí reemplazamos la llamada al método de impresión del campo de tipo con la forma de una expresión de método. Los resultados de salida del programa antes y después del reemplazo son consistentes. Después del cambio, ¿te sientes repentinamente iluminado? Podemos ver claramente cómo se vinculan los parámetros al iniciar una nueva rutina usando la palabra clave go:

● Al iterar datos1, dado que el tipo de elemento en datos1 es un puntero de campo (*campo), v es la dirección del elemento después de la asignación. El parámetro (v) pasado cada vez que se llama a print es en realidad la dirección de cada elemento de campo;

● Al iterar datos2, dado que el tipo de elemento en datos2 es campo (no puntero), es necesario obtener su dirección y luego pasarla. De esta manera, el &v pasado cada vez es en realidad la dirección de la variable v, no la dirección de cada elemento en el segmento data2.

En el artículo 19, aprendimos sobre varias cuestiones clave a las que se debe prestar atención cuando se utiliza for range, incluida la reutilización de variables de bucle. Aquí solo hay una v en todo el proceso de rango, por lo que una vez completada la iteración de datos2, v es una copia del elemento "seis".

De esta manera, una vez que cada rutina secundaria iniciada esté programada para su ejecución cuando la rutina principal llegue al modo de suspensión, cuando las últimas tres rutinas imprima &v, imprimirán el valor "seis" almacenado en v. Cada una de las primeras tres subgorrutinas pasa la dirección de los elementos "uno", "dos" y "tres", y lo que se imprime es "uno", "dos" y "tres".

Entonces, ¿cómo modificar el programa original para que pueda generar ("uno", "dos", "tres", "cuatro", "cinco", "seis") como se esperaba? De hecho, solo necesita cambiar el tipo de receptor del
método de impresión del tipo de campo de *campo a campo.

El lenguaje Go no proporciona soporte gramatical para el mecanismo clásico orientado a objetos, pero implementa métodos de tipo, que se asocian a través del receptor en el lado izquierdo del nombre del método. Elegir el tipo de receptor apropiado para el método de un tipo es una parte importante de la definición del método de Gopher para el tipo.

Supongo que te gusta

Origin blog.csdn.net/hai411741962/article/details/132811280
Recomendado
Clasificación