Golang内置运算符与类型详解

Golang内置运算符与类型详解 我很好奇为二元和一元运算符构建编译时方法是否有意义。我不是语言方面的专家,但我有一些类型,它们具有“加法”、“乘法”等操作的等价物。

我在想,如果能够将类型绑定到运算符,软件代码可能会更清晰易读。我不知道这实现起来会有多困难。

例如,我有一个三维坐标类型,比如:

type c struct {x float64, y float64, z float64}

如果能够这样写就太好了:

var c1, c2 c
c3 := c1+ c2

或者:

var c1 c
var f float64
c1 *= f

再举一个例子。

或者,更理想的情况是能够添加运算符,例如“点积”或“叉积”。

这样做的好处是可以利用所有的运算符优先级等规则。

请注意,我还没有找到关于这个问题的任何讨论,但这并不意味着它不存在,我主要是在思考为什么没有。

我正在考虑替代方案,虽然使用库函数等方法可能不那么笨拙,但它仍然不够优雅。


更多关于Golang内置运算符与类型详解的实战教程也可以访问 https://www.itying.com/category-94-b0.html

9 回复

我花了大约一天的时间来评估泛型,在我看来,它是解决“无自动类型转换”问题的一部分解决方案。我写了一些实验性的代码来使用它们,也许我花的时间不够多,但它们给我的感觉是有些笨拙。

更多关于Golang内置运算符与类型详解的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


Dante_Castagnoli:

Java-bloat that it became. Hopefully, that will not happen with “Go.”

我曾有同事表示,在 Go 中添加泛型是朝着那个方向迈出的不幸一步。我认为泛型可以消除大量无用的代码冗余,但我确实看到它们会增加复杂性,并且可能像运算符重载一样被滥用。这又是更多的权衡。

Dante_Castagnoli:

我还没能找到关于这个问题的任何讨论。

试试 go overload operator - Google Search

你说得对,例如向量运算,在 Go 中确实很难阅读。 另一方面,在其他语言中重载运算符经常被滥用,并导致代码晦涩难懂(参考 Haskell 代码)。 这是一种权衡。

Go 显然不是一门为追求极致书写便利性而设计的语言。语言设计者决定保持类型系统的简洁,力求最少意外。

是的,这会使代码更冗长,但也使其本质上没有“魔法”。代码清晰地表明了它的意图,即便是粗略阅读的人也能理解。无需猜测像 a := b * c * d * e * f 这样的语句中五个 * 运算符各自具体做什么(它们都做同样的事情吗?),也无需疑惑为什么一个期望接收 A 类型的函数会欣然接受 B 类型(是哪种特定的自动转换规则匹配了这种情况?)。对于 == 运算符是“基本”相等运算符还是进行深度相等比较,也不存在疑问(深度有多深?它会跟随指针吗?它能检测图结构中的循环吗?还是会陷入无限循环?)。

我宁愿选择一门冗长的语言,也不愿选择一门在底层有太多“魔法”的语言。

感谢您的指点!我想,这和我正在做的非常相似。

我想我对语言的膨胀感到失望,因为它缺乏类型转换和自动转换。不过对于“C”程序员来说,我最喜欢的面试问题之一是:如果在文件“A”中声明了全局变量 char []x,而在另一个文件中声明了 extern char *x,为什么这是错误的。让我感到奇怪的是,能回答这个问题的人如此之少!我想几十年来我只遇到过一个。

通常,我编写的软件需要在单个实例上处理高达数万个用户输入的并发,因此我关于如何编写该软件的观点有意限制了输入异常,因为异常处理中的错误可能是致命的。此外,虽然我喜欢 Go 的单元测试系统,但除非算法简单,否则它远不足以充分测试这类程序。

我想这就是为什么我从未遇到过进入生产环境(更不用说QA)的类型转换错误,不管这值不值得。

我的观点是,“Go”的严格类型转换使语言变得臃肿,并且更加复杂。另一方面,也许它能找出那些接受更广泛输入范围的程序中的错误。根据我的经验,大多数这类程序都是针对单一最终用户的。

所以,总的来说,我想我不同意这个批评。你总是需要知道(基本)类型。

我确实认为基本类型和其他类型是同一个问题,并且怀疑有一些修改试图绕过这种严格性,但这些修改本身也很痛苦。

我想我并不那么认同“许多错误是由于类型检查而产生的”这一观点,除非同时证明治疗比疾病本身带来的问题更少。

// 代码部分保持原样

好的,那么使用一组运算符来表明它们并非操作标准类型,但沿用相同的优先级等规则,如何?再额外增加一组可以设置优先级等的运算符。另外请注意,虽然我理解消除自动类型转换的目标,但这同样会使语言变得臃肿,并且迫使人使用一套统一的类型,即使这些类型可能并不合适。需要说明的是,我个人从未遇到过类型转换的问题,尽管这只是我的个人经验,可能更多是因为我编写的程序类型所致。

我也了解到,在 Go 语言制定严格规则之前,已经对类型转换进行了评估,并认为它是错误的一个来源。特定的转换方式能改善这些错误吗?我不得不怀疑,同时也必须思考,这种解决方案是否比问题本身更糟糕。

我只是在审视我的代码,并不喜欢它目前这种笨拙的实现方式。

例如:

                    sRads := lib.DegreesToRadians(spin)
                    // p+r(cos t)v1+r(sin t)v2;t real
                    scos, ssin := math.Cos(sRads), math.Sin(sRads)
                    v1Mod := lib.VMult(&vec3Perp, mag * scos)
                    v2Mod := lib.VMult(&vec3Cross, mag * ssin)
                    expect := lib.VAddV(&v1Mod, &v2Mod)
                    expect = lib.VAddV(&expect, &vecs[0])

                    if !lib.VEqual(&vecs[1], &expect) {
                            t.Fatalf("expected \"X\" rotation %s, recevied %s\n", vecs[1].String(), expect.String())
                    }

请注意,我很高兴看到这背后的思考。也许这才是正确的解决方案,只是我不喜欢这些限制,因为它们导致代码无法如我所愿地更简洁、更易读。

如果有更好的方法,我很乐意听取!

感谢您花费时间并深入阐述这些权衡考量,这比我其他地方读到的内容更为详尽。

关于 * a * b * c ... 需要有人去查找基础类型的论点,对我来说并不具有说服力。例如,在 float64(a) * float64(b) * float64(c) 中,人们仍然不知道 a、b、c 的基础类型。

禁止自动转换要求创建明确的转换规则,而不是使用一个基础版本。坦率地说,这似乎是把制定一个具有正确属性的统一版本视为过于困难而放弃了,从而迫使程序员创建自己的规则。

在我看来,不提供标准转换的价值在于迫使程序员意识到转换正在发生。我进一步认为,如果由于疏忽/草率的编程而出现错误,这些错误应该由单元测试来捕获。

对我来说,向程序员指出自动类型转换正在发生,其效用仅在于单元测试未能捕获错误的情况。

我会这样思考这个问题。让 escape 代表这样一组函数:对于它们,单元测试未能捕获自动转换错误,但能捕获所有其他重要的编码/设计错误。

如果 escape 的成本小于为适应“无自动转换”而带来的代码膨胀/可读性下降等代价,那么自动转换就是赢家。请注意,在 Go 中,单元测试的可访问性非常好,单元测试对于许多应用(如云应用)至关重要。在我看来,行业趋势也是减少 QA,将确保软件功能正常的更多责任放在开发人员和设计人员身上。这反过来又增加了对全面单元测试的需求。

我怀疑这种看待权衡的方式是否容易评估,但这就是我的思考方式。


请注意,我非常热爱 Go。我使用 Go 编程大约有 7 年了,对于广泛的应用领域,它相对于 C 和 Python 具有巨大的优势。几十年前,我曾与“绿色团队”的大约四名成员共事,他们猛烈抨击了 Java 后来变得臃肿不堪。希望这种情况不会发生在 Go 身上。

@Dante_Castagnoli

我明白你的观点,但 Go 团队是故意选择不支持运算符重载的:

为什么 Go 不支持方法和运算符的重载?

如果不需要进行类型匹配,方法分派就会简化。其他语言的经验告诉我们,拥有多个同名但签名不同的方法偶尔有用,但在实践中也可能造成混淆且脆弱。仅通过名称匹配并要求类型保持一致,是 Go 类型系统的一个重大简化决策。

关于运算符重载,它似乎更多是一种便利,而非绝对要求。同样,没有它事情会更简单。

此外,这篇来自一个优秀(但已存档)博客的精彩文章中的摘录,展示了运算符重载如何使事情变得更困难,而不是更容易:(虽然讲的是 C++ 与 C,但这个观点适用于任何语言)

当你看到代码

i = j * 5;

……在 C 语言中,你至少知道 j 被乘以 5,结果存储在 i 中。

但如果你在 C++ 中看到同样的代码片段,你什么都不知道。一无所知。在 C++ 中了解真正发生了什么情况的唯一方法是找出 ij 是什么类型,而它们的声明可能完全在别处。这是因为 j 可能是一种重载了 operator* 的类型,当你尝试乘以它时,它会做一些非常巧妙的事情。而 i 可能是一种重载了 operator= 的类型,并且这些类型可能不兼容,因此最终可能会调用一个自动类型转换函数。而找出答案的唯一方法不仅是检查变量的类型,还要找到实现该类型的代码,如果某处有继承,那就只能求老天保佑了,因为现在你必须自己费力地遍历整个类层次结构,试图找到那段代码究竟哪里,如果某处有多态,那你就真的麻烦了,因为仅仅知道 i 和 j 被声明为什么类型是不够的,你必须知道它们现在是什么类型,这可能涉及检查任意数量的代码,而且由于停机问题,你永远无法真正确定自己是否已经查看了所有地方(呼!)。

当你在 C++ 中看到 i=j*5 时,你真的只能靠自己了,伙计,在我看来,这降低了仅通过查看代码来发现潜在问题的能力。

在Go语言中,目前不支持运算符重载或自定义运算符。这是Go设计上的明确选择,旨在保持语言的简洁性和可读性。虽然你描述的场景确实能让代码更简洁,但Go团队认为运算符重载会增加语言的复杂性,并可能导致代码难以理解。

对于你的三维坐标类型,标准的做法是定义方法来实现这些操作。以下是一个示例:

package main

import "fmt"

type Vector struct {
    X, Y, Z float64
}

// 加法
func (v Vector) Add(other Vector) Vector {
    return Vector{v.X + other.X, v.Y + other.Y, v.Z + other.Z}
}

// 标量乘法
func (v Vector) Scale(f float64) Vector {
    return Vector{v.X * f, v.Y * f, v.Z * f}
}

// 点积
func (v Vector) Dot(other Vector) float64 {
    return v.X*other.X + v.Y*other.Y + v.Z*other.Z
}

// 叉积
func (v Vector) Cross(other Vector) Vector {
    return Vector{
        v.Y*other.Z - v.Z*other.Y,
        v.Z*other.X - v.X*other.Z,
        v.X*other.Y - v.Y*other.X,
    }
}

func main() {
    v1 := Vector{1, 2, 3}
    v2 := Vector{4, 5, 6}
    
    // 向量加法
    v3 := v1.Add(v2)
    fmt.Printf("加法: %v\n", v3)
    
    // 标量乘法
    v4 := v1.Scale(2.5)
    fmt.Printf("标量乘法: %v\n", v4)
    
    // 点积
    dot := v1.Dot(v2)
    fmt.Printf("点积: %v\n", dot)
    
    // 叉积
    cross := v1.Cross(v2)
    fmt.Printf("叉积: %v\n", cross)
}

对于复合赋值操作,你可以定义接收指针的方法:

func (v *Vector) AddAssign(other Vector) {
    v.X += other.X
    v.Y += other.Y
    v.Z += other.Z
}

func (v *Vector) ScaleAssign(f float64) {
    v.X *= f
    v.Y *= f
    v.Z *= f
}

func main() {
    v1 := &Vector{1, 2, 3}
    v2 := Vector{4, 5, 6}
    
    v1.AddAssign(v2)
    fmt.Printf("复合加法: %v\n", v1)
    
    v1.ScaleAssign(2)
    fmt.Printf("复合标量乘法: %v\n", v1)
}

虽然这种方法不如直接使用运算符简洁,但它在Go社区中被广泛接受和使用。这种显式的命名方式让代码的意图更加清晰,特别是对于团队协作和代码维护来说。

如果你确实需要更接近数学表达式的语法,可以考虑使用代码生成工具或寻找支持运算符重载的Go方言,但这些都不是Go标准语言的一部分。

回到顶部