2.7 数据类型转换

我们经常利用现有的库函数将数据从一种类型转换为另一种类型。一个很好的例子就是标准数值数据类型。在大多数数学函数中,将日期从整数转换为浮点数是一种常见的用例。

在这一小节中,我们将学习如何在Julia中执行数据类型转换。事实证明,数据类型转换应显式实现。但由于实现了一组默认规则,使得它们可以自动进行转换。

2.7.1 执行简单的数据类型转换

有两种方法可以将值从一种数据类型转换为另一种数据类型。显而易见的选择是从现有值构造一个新对象。例如,我们可以从有理数构造Float64对象,如下所示。

另一种方法是使用convert函数。

两种方法都可以正常工作。在考虑性能优化时,使用convert函数有一个优势,我们将在后面进行解释。

2.7.2 注意有损转换

说到转换,重要的是考虑转换是无损的还是有损的。通常期望数据类型转换是无损的,这意味着,当你从一种类型转换为另一种类型并转换回来时,你将获得相同的值。

由于浮点数的数值表示,这种完美转换并不总是可能的。例如,让我们尝试将1//3转换为Float64,然后再将其转换回Rational。

由于舍入误差,在将1//3转换为Float64类型后无法对其进行重构。该问题不仅限于Rational类型。我们可以通过将值从Int64转换为Float64并将其转换回来轻松地再次解决这一点,如下所示。

我们可以看到这里有精度损失。尽管我们可能对这些结果不太满意,但是只要使用Float64类型,实际上在这里我们就无能为力了。Float64类型是根据IEEE 754浮点规范实现的,并且预期会携带精度错误。如果需要更高的精度,可以改用BigFloat,它可以解决此问题。

在处理浮点值时,我们应谨慎对待精度问题。

2.7.3 理解数字类型转换

出于安全原因,Julia不会自动对数据类型执行转换。每次转换都必须由开发人员显式地指定。

为了使每个人都更容易,Julia默认情况下已经包含数字类型的转换函数。例如,你可以从Base包中找到这段有趣的代码:

这两个函数的第一个参数都是Type{T}类型,其中T是Number的子类型。有效值包括所有标准数字类型,例如Int64、Int32、Float64、Float32等。

让我们进一步了解这两个函数:

·第一个函数表示,只要T是Number的子类型,当我们想将x从T类型转换为T类型(相同类型)时,就很容易返回参数x本身。这可以视为性能优化,因为当目标类型与输入相同时,实际上不需要进行任何转换。

·第二个函数更有趣。为了将Number的子类型x转换为同是Number的子类型T的类型,它仅使用x调用T类型的构造函数。换句话说,此函数可以处理任何Number类型到Number的子类型的另一类型的转换。

你可能想知道为什么我们不首先使用构造函数。这是因为convert函数被设计为可以针对各种常见用例进行自动调用。从前面的内容中可以看到,这种额外的间接访问还使我们在不需要转换时可以绕过构造函数。

convert何时被调用呢?答案是,除了一些特定场景,Julia不会自动执行此操作。我们将在2.7.4节中探讨这些场景。

2.7.4 重温自动转换规则

由于数据类型转换是一个标准操作,因此Julia在以下情况下被设计为自动调用convert函数:

1)赋值给数组会将值转换为该数组的元素类型。

2)赋值给对象的字段会将值转换为该字段的声明类型。

3)使用new构造对象会将值转换为该对象的声明的字段类型。

4)赋值给具有声明类型的变量会将值转换为该类型。

5)具有声明的返回类型的函数会将其返回值转换为该类型。

6)将值传递给ccall会将值转换为相应的参数类型。

让我们确认上面所述的场景。

场景1:给数组赋值

在下面的示例中,将值1分配给Float64数组会将前者转换为浮点值1.0。

场景2:给对象的一个字段赋值

在下面的示例中,Foo结构接受Float64字段。为该字段分配值2时,它将转换为2.0。

场景3:使用new构造一个对象

在下面的示例中,Foo构造函数在创建Foo对象时自动将1转换为1.0。

场景4:给具有声明类型的变量赋值

在下面的示例中,局部变量x被声明为Float64类型。为它分配值1时,它将转换为1.0。

场景5:具有声明的返回类型的函数

在下面的示例中,声明foo函数以返回Float64值。即使return语句显示为1,在返回之前它也会转换为1.0。

场景6:将值传递给ccall

在下面的示例中,来自C语言函数库中的exp函数用于计算指数。它期望将Float64值作为参数,因此当值2传递给ccall时,它会在传递给exp函数之前转换为2.0。

一切都很好,但是似乎又缺少了一些东西。一种最常见的用例:将参数传递给函数是如何处理的呢?如果Julia也自动转换参数,是否不会调用它?答案可能有点令人惊讶。让我们在2.7.5节中更详细地介绍一下。

2.7.5 理解函数分派规则

Julia是一种强类型语言,这意味着开发人员必须非常清楚要传递的类型。仅当函数的参数类型正确匹配时,才能调用该函数(也称为分派)。可以将适当的匹配定义为完全匹配(相同类型)的匹配,或者将要传递的参数是函数签名中期望的子类型时匹配。

为了说明这一点,让我们创建一个函数,将其AbstractFloat类型的参数值加倍。我们将使用subtypetree工具函数快速找到其子类型。

如果我们将整数传递给函数会怎样?事实证明它不太好用。

我们可能天真地认为系统应该将参数自动转换为Float64,然后将该值加倍。但事实并非如此。这不是转换问题。为了获得这种效果,我们显然可以编写另一个带有Int参数的函数,然后将其转换为Float64,之后调用原始函数。但是代码看起来完全一样,这是重复的工作。只需更通用地编写函数即可解决此问题。

如果我们认为参数必须是Number,则可以如下再次限制它。

我们在这里选择做什么取决于我们希望的函数的灵活性。指定一个抽象类型(例如Number)的好处是,我们确信该函数对于实现Number设置的行为的任何类型都可以正常工作。另一方面,如果我们在函数定义中将其保留为非类型,那么只要定义了*运算符,我们就有可能将其他对象传递给函数。

本节中我们学习了如何在Julia中执行数据类型转换。在某些场景中,Julia还可以自动转换数字类型。