Golang类型转换与类型丢失问题探讨

Golang类型转换与类型丢失问题探讨

package main

import (
	"fmt"
)

type IComponent interface {
	GetParent() IComponent
	SetParent(parent IComponent)
	Tick()
}

type IUpdatable interface {
	Update()
}

type Component struct {
	Name   string
	Parent IComponent
}

func (c *Component) GetParent() IComponent {
	return c.Parent
}

func (c *Component) SetParent(parent IComponent) {
	c.Parent = parent
}

func (c *Component) AddChild(child IComponent) {
	child.SetParent(c)

	//其他操作
}

func (c *Component) Tick() {
	fmt.Println("基础 Tick")
}

type ComponentA struct {
	Component
}

type ComponentB struct {
	Component
}

func (c *ComponentB) Update() {
	fmt.Println("ComponentB Update")
}

func (c *ComponentB) Tick() {
	fmt.Println("ComponentB Tick")
	c.Update()
}

func init() {

}

func RunComponent1() {
	fmt.Println("######################RunComponent1##################################")
	child := new(ComponentA)
	child.Name = "child"

	parent := new(ComponentB)
	parent.Name = "parent"
	parent.AddChild(child)
	parent.Tick()

	var updater1 IUpdatable = parent
	updater1.Update()

	a1Parent := child.GetParent()
	if a1Parent == parent {
		fmt.Println("==")
	}
	a1Parent.Tick()

	updater2, ok := a1Parent.(IUpdatable)
	if ok {
		updater2.Update()
	}
}

func RunComponent2() {
	fmt.Println("######################RunComponent2##################################")
	child := new(ComponentA)
	child.Name = "child"

	parent := new(ComponentB)
	parent.Name = "parent"
	parent.Tick()

	child.SetParent(parent)

	var updater1 IUpdatable = parent
	updater1.Update()

	a1Parent := child.GetParent()
	if a1Parent == parent {
		fmt.Println("==")
	}
	a1Parent.Tick()

	updater2, ok := a1Parent.(IUpdatable)
	if ok {
		updater2.Update()
	}
}

func main() {
	RunComponent1()
	RunComponent2()
}

更多关于Golang类型转换与类型丢失问题探讨的实战教程也可以访问 https://www.itying.com/category-94-b0.html

13 回复

问题是什么?

更多关于Golang类型转换与类型丢失问题探讨的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


我个人认为这是一个编译器错误,Go编译器应该将Component替换为ComponentB,因为嵌入的目的是为了减少代码复制。

ComponentB 是 IComponent ComponentB 是 IUpdatable ComponentB.AddChild(child)但 child.GetParent() 不是 (IUpdatable, ComponentB) 但 child.SetParent() 和 GetParent 是 IUpdatable 和 ComponentB

我的问题是关于嵌入,

func (c *Component) AddChild(child IComponent)

c 是 Component,ComponentB 已经丢失,没有面向对象的编程语言是这样的

p:=c.(*ComponentB)

崩溃

p:=c.(*IUpdatable)

崩溃

c 只能是 Component 或 IComponent

func (c *Component) AddChild(child IComponent) {
	child.SetParent(c)

	// 其他代码
}

c 是 ComponentB 吗?但是 child.getParent 不是 ComponentB p:=child.getParent().(*ComponentB)

嵌入可以减少代码复制,而不是继承,我理解这一点 但是 c 不是 ComponentB 吗?为什么

现在,我没有使用嵌入

将 Component 复制到 ComponentB,结果是正确的

type ComponentB struct {
Name   string
Parent IComponent
}
func (c *ComponentB) GetParent() IComponent {
return c.Parent
}
func (c *ComponentB) SetParent(parent IComponent) {
c.Parent = parent
}
func (c *ComponentB) AddChild(child IComponent) {
child.SetParent(c)

    //其他

}

我的英语不好,但我这几天一直在思考你的代码和建议。 我一直在寻找我能接受的解决方案: 第一种:像“C”语言那样,将 addChild(parent, child) 改为全局函数。 第二种:像你说的那样,不调用 addchild,而是调用 SetParent(parent ParentComponent),这类似于“Qt”的做法。

调用 child.SetParent(parent) { child.parent = parent parent.addchild(child) }

func (c *ComponentChild) SetParent(parent Component) {
    fmt.Printf("ComponentChild.SetParent()\n")
    c.BaseComponent.SetParent(parent)
    parent.addchild(child)
}

这个问题让我很困扰,感谢你的帮助!!!

感谢您做了大量工作,请查看以下“C”代码。

#include <stdio.h>

//父结构体
struct BaseComponent
{
struct BaseComponent * parent
};

void add_child(struct BaseComponent * this,struct BaseComponent * child)
{
//add to map/list
child->parent=this;
}

struct BaseComponent * get_parent(struct BaseComponent * this)
{
return this->parent
}

struct ComponentParent
{
struct BaseComponent base_struct;
int a;
};

struct ComponentChild
{
struct BaseComponent base_struct;
int b;
};

int main(void)
{
struct ComponentParent parent;
struct ComponentChild child;

    add_child(&parent,&child);

    struct ComponentParent * child_parent=(struct ComponentParent *)get_parent(&child);

    if(child_parent==&parent)
    {
        printf("ok");
    }

    return 0;
}

我可能会在Go中这样写:

package main

import "fmt"

type Component interface {
	Parent() Component
	SetParent(parent Component)
}

func AddChild(parent, child Component) {
	//add to map/list
	child.SetParent(parent)
}

// 父结构体
type BaseComponent struct {
	parent Component
}

func (c *BaseComponent) Parent() Component          { return c.parent }
func (c *BaseComponent) SetParent(parent Component) { c.parent = parent }

type ComponentParent struct {
	BaseComponent
	a int
}

type ComponentChild struct {
	BaseComponent
	b int
}

func main() {
	var parent ComponentParent
	var child ComponentChild

	AddChild(&parent, &child)

	var childParent *ComponentParent = child.Parent().(*ComponentParent)

	if childParent == &parent {
		fmt.Println("ok")
	}

	var childParent2 = child.Parent()
	if childParent2 == &parent {
		fmt.Println("also ok")
	}
}

很好的例子。

我在末尾添加了这段代码:

	fmt.Printf("%v\n", child.parent == &parent)
	fmt.Printf("%v\n", child.BaseComponent.parent == &parent)

这表明 ComponentChild “继承”了 BaseComponentparent 字段,因此可以像这样使用:child.parent,它等同于 child.BaseComponent.parent

我还添加了这段代码,它仍然可以正常工作:

func (c *ComponentChild) SetParent(parent Component) {
	fmt.Printf("ComponentChild.SetParent()\n")
	c.BaseComponent.SetParent(parent)
}

ComponentParentComponentChildBaseComponent “继承”了方法(Parent()SetParent())。如果 ComponentParent 定义了自己的 SetParent() 方法,那么当 *ComponentParent 作为 Component 接口类型传递时,将使用它自己的方法。

请注意,Go 语言没有像 Java 或 C++ 等其他语言中定义的“虚”方法或“抽象”方法。在 Go 语言中,等效的做法是定义并使用一个显式的接口(在本例中是 Component)。你可以在需要接口类型的地方使用结构体指针,并且可以将 interface{} 类型转换回其关联的结构体指针,但不能将一个结构体指针转换为另一个结构体指针。Go 将多态性(接口类型)与继承(在一个结构体中嵌入另一个结构体)分离开来。我认为这是一件好事。

看起来你试图在Go中使用继承,但Go没有继承;只有嵌入。

当你这样写:

type ComponentB struct {
	Component
}

ComponentB 并没有“扩展” ComponentComponentB 包装Component

在这一行:

	parent := new(ComponentB)
	parent.Name = "parent"
	parent.AddChild(child)	// <<<
	parent.Tick()

其实现是这样的:

// 注意这个 ↓ 是 `Component`,不是 `ComponentB`
func (c *Component) AddChild(child IComponent) {
	child.SetParent(c)

	// 其他代码
}

所以当你调用 parent.AddChild(child) 时,它实际上是 parent.Component.AddChild(child) 的语法糖,然后会将 parent.Component 作为 child 的父级添加;而不是 parent

正确的解决方案取决于上下文。在你的例子中,我没有看到 AddChild 的目的是什么。 看起来直接调用 child.SetParent(parent) 并去掉 AddChild 是合理的。如果你实际代码中的 AddChild 除了调用 SetParent 之外还做了其他事情,你能展示一下那段代码吗?如果它依赖于父级的实现,那么你可能需要在父级上实现 AddChild。如果所有组件实现的代码都相同,那么就把它做成一个顶层函数:

func AddChild(parent, child IComponent) {
    // 你想要的任何公共初始化
    child.SetParent(parent)
    // 也许这里还有额外的代码?
}

附注:在我看来,你试图用Go的语法来写C#代码,但它们是两种不同的语言,通常不能逐行翻译,就像C#和JavaScript或TypeScript等一样……

我建议查看 Go语言之旅 - 方法和接口。前面的一些幻灯片你可以快速浏览,但我建议从 幻灯片 #9 开始阅读关于接口的部分,以了解它们与C#的不同之处。

这不是编译器错误。Go 语言是故意这样设计的。

dqpmchtx: c 是 Component,ComponentB 丢失了,没有面向对象的编程语言是这样的

有些人说 Go 不是面向对象的语言。我个人认为它是面向对象的,但它没有继承。与你的 Go 代码等价的 C# 代码(减去 RunComponent2 的实现)是这样的:

C# 代码
using System;

RunComponent1();
//RunComponent2();

public interface IComponent
{
	IComponent GetParent();
	void SetParent(IComponent parent);
	void Tick();
}

public interface IUpdatable
{
	void Update();
}

public class Component : IComponent
{
	public string Name;
	public IComponent Parent;

	public IComponent GetParent() => Parent;
	public void SetParent(IComponent parent) => this.Parent = parent;
	public void AddChild(IComponent child) => child.SetParent(this);
	public void Tick() => Console.WriteLine("base Tick");
}

public class ComponentA : IComponent
{
	public readonly Component Component = new Component();

	public string Name { get => Component.Name; set => Component.Name = value; }
	public IComponent Parent { get => Component.Parent; set => Component.Parent = value; }
	public IComponent GetParent() => Component.GetParent();
	public void SetParent(IComponent parent) => Component.SetParent(parent);
	public void AddChild(IComponent child) => Component.AddChild(child);
	public void Tick() => Component.Tick();
}

public class ComponentB : IComponent, IUpdatable
{
	public readonly Component Component = new Component();

	public string Name { get => Component.Name; set => Component.Name = value; }
	public IComponent Parent { get => Component.Parent; set => Component.Parent = value; }
	public IComponent GetParent() => Component.GetParent();
	public void SetParent(IComponent parent) => Component.SetParent(parent);
	public void AddChild(IComponent child) => Component.AddChild(child);

	public void Update() => Console.WriteLine("ComponentB Update");
	public void Tick()
	{
		Console.WriteLine("ComponentB Tick");
		Update();
	}
}

void RunComponent1()
{
	Console.WriteLine("#####################RunComponent1##################################");
	var child = new ComponentA();
	child.Name = "child";

	var parent = new ComponentB();
	parent.Name = "parent";
	
	parent.AddChild(child);
	parent.Tick();

	IUpdatable updater1 = parent;
	updater1.Update();

	var a1Parent = child.GetParent();
	if (a1Parent == parent) {
		Console.WriteLine("==");
	}
	a1Parent.Tick();

	if (a1Parent is IUpdatable updater2)
	{
		updater2.Update();
	}
}

/*
func RunComponent2() {
fmt.Println(“#####################RunComponent2##################################”)
child := new(ComponentA)
child.Name = “child”

parent := new(ComponentB)
parent.Name = "parent"
parent.Tick()

child.SetParent(parent)

var updater1 IUpdatable = parent
updater1.Update()

a1Parent := child.GetParent()
if a1Parent == parent {
	fmt.Println("==")
}
a1Parent.Tick()

updater2, ok := a1Parent.(IUpdatable)
if ok {
	updater2.Update()
}
}
*/

请注意我写的是:

public class ComponentA
{
    public readonly Component Component = new Component();
}

而不是

public class ComponentA : Component { }

因为 Go 没有继承。

这是一个典型的Go语言接口类型转换问题。问题出现在RunComponent1()中,当通过AddChild方法设置父组件后,子组件存储的父组件类型信息丢失了。

问题分析

RunComponent1()中:

parent := new(ComponentB)  // 实际类型是 *ComponentB
parent.AddChild(child)     // 这里将 parent 作为 IComponent 接口传递

AddChild方法中:

func (c *Component) AddChild(child IComponent) {
    child.SetParent(c)  // c 是 *Component 类型,不是 *ComponentB
    // 其他操作
}

这里发生了类型丢失*ComponentB被转换为IComponent接口,然后存储在子组件的Parent字段中,丢失了*ComponentB特有的方法。

解决方案

方案1:使用类型断言恢复类型

func RunComponent1() {
    fmt.Println("######################RunComponent1##################################")
    child := new(ComponentA)
    child.Name = "child"

    parent := new(ComponentB)
    parent.Name = "parent"
    parent.AddChild(child)
    parent.Tick()

    var updater1 IUpdatable = parent
    updater1.Update()

    a1Parent := child.GetParent()
    if a1Parent == parent {
        fmt.Println("==")
    }
    
    // 尝试恢复为 *ComponentB
    if compB, ok := a1Parent.(*ComponentB); ok {
        compB.Tick()  // 现在可以调用 ComponentB 的 Tick 方法
        if updater, ok := compB.(IUpdatable); ok {
            updater.Update()
        }
    } else {
        a1Parent.Tick()  // 只能调用基础的 Tick 方法
    }
}

方案2:修改 AddChild 方法签名

func (c *ComponentB) AddChild(child IComponent) {
    child.SetParent(c)  // 这里 c 仍然是 *ComponentB 类型
    // 其他操作
}

// 或者使用泛型(Go 1.18+)
type Component[T any] struct {
    Name   string
    Parent T
}

func (c *Component[T]) SetParent(parent T) {
    c.Parent = parent
}

方案3:使用组合模式保持类型信息

type TypedComponent struct {
    Component
    ConcreteParent interface{}
}

func (c *Component) AddChildWithType(child IComponent, parent interface{}) {
    child.SetParent(c)
    if typed, ok := child.(interface{ SetConcreteParent(interface{}) }); ok {
        typed.SetConcreteParent(parent)
    }
}

type ComponentA struct {
    Component
    ConcreteParent interface{}
}

func (c *ComponentA) SetConcreteParent(parent interface{}) {
    c.ConcreteParent = parent
}

运行结果分析

当前代码的输出会是:

######################RunComponent1##################################
ComponentB Tick
ComponentB Update
ComponentB Update
==
基础 Tick  # 这里调用了 Component 的 Tick,不是 ComponentB 的 Tick

######################RunComponent2##################################
ComponentB Tick
ComponentB Update
ComponentB Update
==
基础 Tick  # 同样的问题

这是因为通过接口存储后,类型信息被擦除,只能调用接口定义的方法。要调用具体类型的方法,必须通过类型断言恢复原始类型。

回到顶部