使用跨平台应用中的本机类型

本文介绍如何在跨平台应用程序中使用新的 iOS 统一 API 本机类型(nint、nuint、nfloat),其中代码与非 iOS 设备(例如 Android 或 Windows Phone 操作系统)共享

有 64 种本机类型可与 iOS 和 Mac API 配合使用。 如果你正在编写的共享代码也会在 Android 或 Windows 上运行,则需要管理统一类型到可以共享的常规 .NET 类型的转换。

本文档讨论了通过共享/通用代码与统一 API 进行互操作的不同方法。

何时使用本机类型

Xamarin.iOS 和 Xamarin.Mac 统一 API 仍包括 intuintfloat 数据类型,以及 RectangleFSizeFPointF 类型。 这些现有的数据类型应继续在任何共享的跨平台代码中使用。 仅当调用需要支持体系结构感知类型的 Mac 或 iOS API 时,才应使用新的本机数据类型。

根据共享代码的性质,跨平台代码有时可能需要处理 nintnuintnfloat 数据类型。 例如:处理矩形数据转换的库以前使用 System.Drawing.RectangleF 在应用的 Xamarin.iOS 和 Xamarin.Android 版本之间共享功能,需要更新才能处理 iOS 上的本机类型。

如何处理这些更改取决于应用程序的大小和复杂性,以及已使用的代码共享形式,正如我们将在以下部分中看到的那样。

代码共享注意事项

共享代码选项文档中所述,在跨平台项目之间共享代码有两种主要方式:共享项目和可移植类库。 使用这两种类型中的一种会限制我们在跨平台代码中处理本机数据类型时的选项。

可移植类库项目

可移植类库 (PCL) 允许你定位希望支持的平台,并使用接口来提供特定于平台的功能。

由于 PCL 项目类型被编译为 .DLL 并且它没有统一 API 的意义,因此你将被迫在调用前端应用程序中的 PCL 类和方法的类型强制转换中,继续使用现有的数据类型(intuintfloat)。 例如:

using NativePCL;
...

CGRect rect = new CGRect (0, 0, 200, 200);
Console.WriteLine ("Rectangle Area: {0}", Transformations.CalculateArea ((RectangleF)rect));

共享项目

共享资产项目类型允许将源代码组织在单独的项目中,然后将其包含并编译到各个特定于平台的前端应用中,并根据需要使用 #if 编译器指令来管理特定于平台的要求。

在跨平台共享资产项目中选择支持本机数据类型的方法时,需要考虑使用共享代码的前端移动应用程序的大小和复杂性,以及共享代码的大小和复杂性。

根据这些因素,可以使用 if __UNIFIED__ ... #endif 编译器指令来实现以下类型的解决方案,以处理统一 API 特定的代码更改。

使用复制方法

以对上面指定的矩形数据执行转换的库为例。 如果库仅包含一两个非常简单的方法,你可以选择为 Xamarin.iOS 和 Xamarin.Android 创建这些方法的重复版本。 例如:

using System;
using System.Drawing;

#if __UNIFIED__
using CoreGraphics;
#endif

namespace NativeShared
{
    public class Transformations
    {
        #region Constructors
        public Transformations ()
        {
        }
        #endregion

        #region Public Methods
        #if __UNIFIED__
            public static nfloat CalculateArea(CGRect rect) {

                // Calculate area...
                return (rect.Width * rect.Height);

            }
        #else
            public static float CalculateArea(RectangleF rect) {

                // Calculate area...
                return (rect.Width * rect.Height);

            }
        #endif
        #endregion
    }
}

在上面的代码中,由于 CalculateArea 例程非常简单,因此我们使用了条件编译并创建了该方法的单独统一 API 版本。 另一方面,如果库包含许多例程或多个复杂例程,则此解决方案将不可行,因为它会出现保持所有方法同步以进行修改或 bug 修复的问题。

使用方法重载

在这种情况下,解决方案可能是使用 32 位数据类型创建方法的重载版本,以便它们现在将 CGRect 作为参数和/或返回值,将该值转换为 RectangleF(知道从 nfloat 转换到 float 是有损转换),并调用例程的原始版本来完成实际工作。 例如:

using System;
using System.Drawing;

#if __UNIFIED__
using CoreGraphics;
#endif

namespace NativeShared
{
    public class Transformations
    {
        #region Constructors
        public Transformations ()
        {
        }
        #endregion

        #region Public Methods
        #if __UNIFIED__
            public static nfloat CalculateArea(CGRect rect) {

                // Call original routine to calculate area
                return (nfloat)CalculateArea((RectangleF)rect);

            }
        #endif

        public static float CalculateArea(RectangleF rect) {

            // Calculate area...
            return (rect.Width * rect.Height);

        }

        #endregion
    }
}

同样,只要精度损失不会影响应用程序特定需求的结果,这就是一个很好的解决方案。

使用别名指令

对于存在精度损失问题的区域,另一种可能的解决方案是使用 using 指令为 Native 和 CoreGraphics 数据类型创建别名,方法是将以下代码添加到共享源代码文件的顶部并将任何所需的 intuintfloat 值转换为 nintnuintnfloat

#if __UNIFIED__
    // Mappings Unified CoreGraphic classes to MonoTouch classes
    using RectangleF = global::CoreGraphics.CGRect;
    using SizeF = global::CoreGraphics.CGSize;
    using PointF = global::CoreGraphics.CGPoint;
#else
    // Mappings Unified types to MonoTouch types
    using nfloat = global::System.Single;
    using nint = global::System.Int32;
    using nuint = global::System.UInt32;
#endif

这样,我们的示例代码就变成了:

using System;
using System.Drawing;

#if __UNIFIED__
    // Map Unified CoreGraphic classes to MonoTouch classes
    using RectangleF = global::CoreGraphics.CGRect;
    using SizeF = global::CoreGraphics.CGSize;
    using PointF = global::CoreGraphics.CGPoint;
#else
    // Map Unified types to MonoTouch types
    using nfloat = global::System.Single;
    using nint = global::System.Int32;
    using nuint = global::System.UInt32;
#endif

namespace NativeShared
{

    public class Transformations
    {
        #region Constructors
        public Transformations ()
        {
        }
        #endregion

        #region Public Methods
        public static nfloat CalculateArea(RectangleF rect) {

            // Calculate area...
            return (rect.Width * rect.Height);

        }
        #endregion
    }
}

请注意,此处我们更改了 CalculateArea 方法以返回 nfloat,而不是标准的 float。 这样做是为了避免在尝试将计算的nfloat 结果隐式转换为 float 返回值时出现编译错误(因为相乘的两个值的类型为 nfloat)。

如果代码在非统一 API 设备上编译并运行,则 using nfloat = global::System.Single; 会将 nfloat 映射到 Single,后者将隐式转换为 float,从而允许使用前端应用程序调用 CalculateArea 方法且无需修改。

在前端应用中使用类型转换

如果前端应用程序仅对共享代码库进行少量调用,则另一种解决方案可能是保持库不变,并在调用现有例程时在 Xamarin.iOS 或 Xamarin.Mac 应用程序中进行类型转换。 例如:

using NativeShared;
...

CGRect rect = new CGRect (0, 0, 200, 200);
Console.WriteLine ("Rectangle Area: {0}", Transformations.CalculateArea ((RectangleF)rect));

如果使用应用程序对共享代码库进行数百次调用,则这可能不是一个好的解决方案。

根据我们的应用程序体系结构,我们最终可能会使用一种或多种上述解决方案来支持跨平台代码中的本机数据类型(如果需要)。

Xamarin.Forms 应用程序

若要将 Xamarin.Forms 用于跨平台 UI(也将与统一 API 应用程序共享),需要满足以下条件:

  • 整个解决方案必须使用 Xamarin.Forms NuGet 包版本 1.3.1(或更高版本)。
  • 对于任何 Xamarin.iOS 自定义渲染器,请根据 UI 代码的共享方式(共享项目或 PCL)使用上面提供的相同类型的解决方案。

与标准跨平台应用程序一样,在大多数情况下,应在任何共享的跨平台代码中使用现有的 32 位数据类型。 仅当调用需要支持体系结构感知类型的 Mac 或 iOS API 时,才应使用新的本机数据类型。

有关更多详细信息,请参阅更新现有的 Xamarin.Forms 应用文档。

总结

在本文中,我们已了解何时在统一 API 应用程序中使用本机数据类型及其产生的跨平台影响。 我们提出了多种解决方案,可用于必须在跨平台库中使用新的本机数据类型的情况。 此外,我们还快速学习了有关在 Xamarin.Forms 跨平台应用程序中支持统一 API 的指南。