实际编码中,经常会出现类型转换的情况。

1.C#中的类型转换

C#中有两种类型转换:隐式类型转换显示类型转换(也作强制转换),其中隐式转换主要是在整型、浮点型之间的转换,将存储范围小的数据类型直接转换成存储范围大的数据类型,也就是小转大

int a = 100;
double d = a;  //将int类型转换为double类型
float f = 3.14f;
d = f;    //将float类型转换为double类型

反之

int r ;
double rd=5.0;
r = rd;
Cannot implicitly convert type 'double' to 'int'. An explicit conversion exists (are you missing a cast?)	

不能隐式转换doubleint:因为进行转换可能会导致信息丢失,则编译器会要求执行显式转换,显式转换也称为强制转换

int r ;
double rd=5.0;
r = (int)rd;

形为 (T)E 的强制转换表达式将表达式 E 的结果显式转换为类型 T

  • 如果不存在从类型 E 到类型 T 的显式转换,则发生编译时错误。
  • 在运行时,显式转换可能不会成功,强制转换表达式可能会引发异常。

有关支持的显式数值转换的完整列表,请参阅内置数值转换一文的显式数值转换部分。

对于引用类型,从基类型转换为派生类型,则必须进行显式强制转换

// 创建派生类
Giraffe g = new Giraffe();

// 隐式转换为基类是安全的
Animal a = g;

// 需要显式转换才能强制把基类转换回派生类型
Giraffe g2 = (Giraffe)a; //如果a不是Giraffe,编译能通过,但在运行时会抛出异常

另外一种特殊的类型转换是装箱和拆箱,这里就不做介绍了。

2.Go语言中的类型转换

Go语言没有隐式转换,只有显式转换,说白了,任何一种类型转换,都需要开发者进行手动操作。

简单转换

何谓简单转换?就是转换数据类型的方式很简单。

func main() {
	a := 10
	b := float32(a)

	c := 50.5
	d := int32(c)
	e := int64(c)
	fmt.Printf("%v %T\n", a, a)
	fmt.Printf("%v %T\n", b, b)
	fmt.Printf("%v %T\n", c, c)
	fmt.Printf("%v %T\n", d, d)
	fmt.Printf("%v %T\n", e, e)
}
输出
10 int
10 float32
50.5 float64
50 int32
50 int64

注意事项

  • 不是所有数据类型都能转换的,例如string类型转换为int肯定会失败,编译就会报错cannot convert xxx (type string) to type int64
  • 低精度转换为高精度时是安全的,高精度的值转换为低精度时会丢失精度。上面的变量d与e就是这种情况;
  • 要跨大类型转换,例如string与int的互转,可以使用strconv包提供的函数

3.strconv包

这一节就归纳一些在实际开发中,strconv包中经常用到的函数:

3.1 Itoa()

Itoa()函数用于将int类型数据转换为对应的字符串表示,具体的函数签名如下。

func Itoa(i int) string

实际开发中,组合生成redis的key,key值为int类型的id

func main() {
    var userid int
    KeyPostVotedZSetPF := "post:voted:"
    userid = 41654
    key := KeyPostVotedZSetPF + strconv.Itoa(userid)
    println(key)
}
输出
post:voted:41654

3.2 FormatInt()

可以看到Itoa()接收的是int类型的参数,但是我们如果是通过雪花算法生成的用户id,是int64,那Itoa显然不能使用,FormatInt() 函数实现了将int64数据格式化为string,具体的函数签名如下:

func FormatInt(i int64, base int) string
func main() {	
    var userid int64
    KeyPostVotedZSetPF := "post:voted:"
    userid = 41654489498
    key := KeyPostVotedZSetPF + strconv.FormatInt(userid, 10)
    println(key)
}
输出
post:voted:41654489498

Format系列函数

Format其实是有一系列函数,用于实现了将给定类型数据格式化为string类型数据的功能。

FormatBool()
func FormatBool(b bool) string
FormatInt()
func FormatInt(i int64, base int) string
FormatUint()
func FormatUint(i uint64, base int) string

是FormatInt的无符号整数版本。

FormatFloat()
func FormatFloat(f float64, fmt byte, prec, bitSize int) string

函数将浮点数表示为字符串并返回。

bitSize表示f的来源类型(32:float32、64:float64),会据此进行舍入。

fmt表示格式:’f’(-ddd.dddd)、’b’(-ddddp±ddd,指数为二进制)、’e’(-d.dddde±dd,十进制指数)、’E’(-d.ddddE±dd,十进制指数)、’g’(指数很大时用’e’格式,否则’f’格式)、’G’(指数很大时用’E’格式,否则’f’格式)。

prec控制精度(排除指数部分):对’f’、’e’、’E’,它表示小数点后的数字个数;对’g’、’G’,它控制总的数字个数。如果prec 为-1,则代表使用最少数量的、但又必需的数字来表示f。

3.3 Parse系列函数

有了Format系列函数把一些类型转换为string类型,那么反过来Parse系列函数就是用于将字符串类型转换为给定类型的值

ParseBool()
func ParseBool(str string) (value bool, err error)

返回字符串表示的bool值。它接受1、0、t、f、T、F、true、false、True、False、TRUE、FALSE;否则返回错误。

ParseInt()
func ParseInt(s string, base int, bitSize int) (i int64, err error)

返回字符串表示的整数值,接受正负号。

base指定进制(2到36),如果base为0,则会从字符串前置判断,”0x”是16进制,”0”是8进制,否则是10进制;

bitSize指定结果必须能无溢出赋值的整数类型,0、8、16、32、64 分别代表 int、int8、int16、int32、int64;

返回的err是*NumErr类型的,如果语法有误,err.Error = ErrSyntax;如果结果超出类型范围err.Error = ErrRange。

ParseUnit()
func ParseUint(s string, base int, bitSize int) (n uint64, err error)

ParseUint类似ParseInt但不接受正负号,用于无符号整型。

ParseFloat()
func ParseFloat(s string, bitSize int) (f float64, err error)

3.4 Atoi()

有了int类型转字符串类型,就有字符串类型转int类型Atoi()函数用于将字符串类型的整数转换为int类型,函数签名如下。

func Atoi(s string) (i int, err error)

如果传入的字符串参数无法转换为int类型,就会返回错误。

s1 := "100"
i1, err := strconv.Atoi(s1)
if err != nil {
	fmt.Println("can't convert to int")
} else {
	fmt.Printf("type:%T value:%#v\n", i1, i1) //type:int value:100
}

除了上面常用的Format系列函数和Parse系列函数,还有Append系列函数,用法与Format类似,只是多了追加的操作。更多操作就看官方文档

4.接口类型断言

Go语言的空接口可以存储任意类型的值,那我们如何获取其存储的具体数据呢?

在接口值上的操作,用于检查接口类型变量所持有的值是否实现了期望的接口或者具体的类型

value, ok := x.(T)

x 表示一个接口类型的值(包括空接口),T 表示一个具体的类型(也可为接口类型)。

当变量是一个接口时,相比较具体类型时:

  • 具体类型→动态类型
  • 具体类型的值→动态值
func main() {
	var x interface{}
	x = "I'm Garfield"
	v, ok := x.(string)
	if ok {
		fmt.Println(v)
	} else {
		fmt.Println("Asserts Failed")
	}
}

上面的示例中如果要多次断言就需要多个if判断,Go语言中中提供了另外一种断言方法switch:变量x断言成了type 类型,type 类型具体值就是 switch case 的值,如果 x 成功断言成了某个 case 类型,就可以执行那个 case,此时 i := x.(type) 返回的 i 就是那个类型的变量了,可以直接当作 case 类型使用

func justifyType(x interface{}) {
	switch i := x.(type) {
	case int64:
		fmt.Printf("x is a int64, is %v\n", i)
	case string:
		fmt.Printf("x is a string,value is %v\n", i)
	case int:
		fmt.Printf("x is a int is %v\n", i)
	case bool, int32:
		fmt.Printf("x is a bool or int32 is %v\n", i)
	case nil:
		fmt.Printf("x is a interface{} is %v\n", i)
	default:
		fmt.Println("unsupport type!")
}

func main() {
    justifyType(nil)
    justifyType("I'm Garfield")
    justifyType(44)
    justifyType(int64(516165161616))
    justifyType(true)
    justifyType(int32(1105020))
}

5.接口类型检测

除此之外,开发者还可以像C#那样把实现了接口的实例赋值给接口变量,前面博文中介绍过利用编译器和匿名变量,判断结构体是否实现了接口,实质也就是利用这种方式做一个接口类型检测:

var _ IRouter = &Engine{}

类似的还有:

var _ IRouter = (*Engine)(nil)

nil的类型是 nil,地址值为0x0,利用强制类型转换成了 *Engine ,返回的变量就是类型为 *Engine 地址值为0x0,如果 *Engine 没有实现了 IRouter 接口,就会在编译时报错,上面两个看似不同的代码,其本质是一样的,目的也一致:实现在编译期间检测接口是否实现

参考链接

https://docs.microsoft.com/zh-cn/dotnet/csharp/programming-guide/types/casting-and-type-conversions

https://docs.microsoft.com/zh-cn/dotnet/csharp/language-reference/operators/type-testing-and-cast#cast-expression