.NET Core Framework

通过 .NET Framework 实现跨平台

Cheryl Simmons

大多数开发人员更喜欢一次性编写好业务逻辑代码,以后再重用这些代码。与构建不同的应用以面向多个平台相比,这种方法更加容易。有关 .NET Core(Microsoft .NET Framework 的组件化版本)的最新声明以及 Microsoft 和 Xamarin 之间的亲密合作伙伴关系表示,如果您创建与 .NET Core 兼容的可移植类库 (PCL),那么现在比以往任何时候都更接近于这一现实。

但是,现有的 .NET Framework 库怎么办呢?要使这些跨平台兼容并将它们转换成 PCL,需要做多少工作呢?输入 .NET Portability Analyzer。利用几个简单的技术并进行一些项目文件修改,这有助于简化这一流程。

.NET Portability Analyzer 工具是 .NET Framework 团队创建的 Visual Studio 扩展。您可以与支持扩展的 Visual Studio 的任何最新版本配合使用该工具。只需将 Portability Analyzer 指向您的程序集或项目,该工具就会针对您为了提高兼容性而应使用的 API 提供摘要、详细报告和建议。对于项目,该工具会列出错误消息并将您转到需要更改的代码行。该工具还可以提供主要 Microsoft 平台的结果,并且您可以对其进行配置以提供其他平台(如 Mono 和 Xamarin)的结果。

.NET Portability Analyzer 含有一个称作 API Portability Analyzer 的同级控制台应用(您可以从 aka.ms/w1vcle 中下载该应用),它生成的结果类似于 Portability Analyzer 生成的结果。在本文中,我将着重介绍如何使用 Visual Studio 扩展。同样值得注意的是,.NET Portability Analyzer 在本文编写日期与本文发布日期之间的这段时间内可能进行了某些更新,因此该工具的外观可能与您在此处看到的图像不同。

进行相应设置以取得成功

对于要成功跨平台采用的库,它应适当分解且包含大部分业务逻辑。UI 代码应被分离到其他项目。不过,由于 .NET Core 是 .NET Framework 的子集,因此,即使对您的代码进行适当分解,您的库可能正在使用 .NET Core 中不支持的 API。

在某些情况下,存在可以完成同样事情的备用 API。在这些情况下,Portability Analyzer 将提议使用一个备用 API。在其他情况下,没有 API 可以替代而且您需要分解出特定于平台的代码。最后,即使您不知道程序集的分解情况,也可以使用 Portability Analyzer 来执行快速评估。

若要使用该扩展,您需要 Visual Studio 2013 或 2014。下一步是安装该扩展。在 Visual Studio 库中搜索 .NET Portability Analyzer 或直接转到 aka.ms/lah74y 即可找到它。

单击“下载”按钮并选择“打开”。通过下一个对话框,您可以选择要将该扩展应用到的 Visual Studio 版本。单击“安装”可启动安装,然后单击“关闭”可退出该对话框。现在,您可以选择您的目标平台并分析程序集或项目。

选择目标平台

默认情况下,Portability Analyzer 提供 .NET Framework、ASP.NET vNext(又称 .NET Core)、Windows 和 Windows Phone 的结果。您可以通过以下步骤指定其他选项:访问 Visual Studio 中“工具”|“选项”菜单中的 .NET Portability Analyzer 条目并选择您要面向的平台集(如图 1 中所示)。

为您的项目选择目标平台
图 1 为您的项目选择目标平台

运行 Portability Analyzer

有两种方法可以用来分析程序集和项目:

  • 若要分析已构建的程序集或可执行文件,请从 Visual Studio 的“分析”菜单中访问 Portability Analyzer 并浏览至程序集位置。使用此选项,该工具会生成摘要和详细报告。
  • 若要分析项目,请右键单击解决方案资源管理器中的目标项目。选择“分析”|“分析程序集可移植性”(请参见图 2),这特定于您所选的项目。使用此选项,该工具会生成摘要、详细报告并将消息输出到提供文件名和问题发生所在行号的错误列表。您还可以双击每条消息,然后该工具会将您导航到指定的代码行。

针对特定项目选择“分析程序集可移植性”
图 2 针对特定项目选择“分析程序集可移植性”

为了测试该工具,我打开了大约一年前从事的一个项目(面向 Windows Phone Silverlight 8 的一个文字游戏)。我以面向 Windows Phone 8 和 Windows 8 的可移植类库 (PCL) 中的业务逻辑开始。我的想法是重用该库以实现适用于 Windows 的相同应用。但是,我遇到了一些问题并陷入慌乱中,因此我改变了方法,并且我的库现在只面向 Windows Phone Silverlight 8。

我的计划是分析该库的可移植性,进行必要的代码更改,然后将该项目转换为 PCL 项目并重新瞄准 Windows 8.1、Windows Phone 8.1 和 Windows Phone Silverlight 8.1。我还想要使用 .NET Core 测试它的兼容性。Portability Analyzer 可以让我一览我需要做的工作,而无需实际转换该项目、更改目标以及试图解决编译错误。我很期待地想看看,我是否可以使用自己的库来构建 Android 应用,因此我对该工具进行了配置以便也提供 Xamarin.Android 的结果。

我运行该工具,并且结果真是鼓舞人心。图 3 显示摘要、详细报告、错误消息和报告 URL。根据摘要,我发现我的库与所有这些平台都非常兼容。它与 Windows Phone Silverlight 8.1 完全兼容,但这并不令人十分惊讶,因为考虑到 Windows Phone Silverlight 8 是最初目标。

显示兼容平台的详细兼容性报告
图 3 显示兼容平台的详细兼容性报告

详细结果通过类似电子表格的形式仅显示一个或多个目标平台不支持的 API。可轻松扫描详细信息。它们标有一个红色的 X,用来指明不支持 API 的位置,而绿色的标记则指明支持。值得注意的是,各个平台均支持且无需任何重构的 API 不会在此报告中列出。

详细信息还包括推荐的更改内容列,其中指向可跨多个平台工作的备用 API。在详细信息的底部,该报告包含“返回到摘要”链接。这将导航回到顶部的摘要。虽然我的结果非常短,但是“返回到顶部”功能对于较长的报告非常有用。

由于我已经分析了一个项目,因此我的报告包含指明文件和发生使用的行号的“错误列表”消息。如果单击该消息,此工具将转到该消息指明的文件和行。

如果您要在 Visual Studio 外部访问这些结果,可以在位于和目标程序集相同的项目目录中的 HTML 文件 (ApiPortability­Analysis.htm) 中找到这些结果。该位置在报告顶部中的 URL 部分中指明,如图 4 中所示。

存储的可从 Visual Studio 外部访问的可移植性分析结果
图 4 存储的可从 Visual Studio 外部访问的可移植性分析结果

工作进展

随着 .NET 团队收集更多信息,.NET Portability Analyzer 及其结果和指南会随时发生变化(请参见图 5)。当您使用该工具或它的控制台应用的对等应用时,该团队会收集有关 API 使用情况的匿名数据,并汇总到 bit.ly/14vciaD

此站点显示 .NET API 使用级别
图 5 此站点显示 .NET API 使用级别

默认情况下,此站点向您显示需要最多代码更改的 API。您可以通过更改“显示”下拉列表(图像中未显示)中的值对此进行更改。您还可以将鼠标悬停在“R”或“S”图标上,查看由 Portability Analyzer 显示的相同代码建议。

列表中的每个 API 名称是将您转到报告该 API 在哪些平台上受支持的页面的链接。该团队打算继续更新该站点并且希望将其开放给客户发布内容,因此要定期进行检查。

跨平台执行库

现在,是时候修复我的库了。我知道我希望我的库可以与 Windows Phone 8.1 和 Windows 8.1 一起使用,而且我发现了一些问题。对于每个问题,我将采取不同的方法。

使用平台抽象 有两个资源流相关的条目。根据该报告,Windows 8.1 或 Windows Phone 8.1 不支持这些 API。该工具未推荐任何我可以替代的 API,因此我需要将此代码从库中删除。该报告告知我这两个成员用在相同的几行代码中,其会加载一个静态词典文件:

WordList = new ObservableCollection<string>();
StreamResourceInfo info =
  Application.GetResourceStream(new
  Uri("/WordLibWP;component/US.dic", UriKind.Relative));
StreamReader reader = new StreamReader(info.Stream);
string line;
while ((line = reader.ReadLine()) != null)
{
  WordList.Add(line);
}
reader.close();

我知道我可以在 Windows 8.1 和 Windows Phone 8.1 上使用 Windows.Storage API 来加载资源文件,但是我的库将无法与 Windows Phone Silverlight 兼容。为了覆盖最广范围,我决定对这段特定于平台的代码进行抽象化。平台抽象是非常有用的技术,可提供定义行为的库,但是在特定于平台的代码中以不同的方式实现该行为。如果您希望您的代码可以跨平台兼容,则应熟悉此项技术。

以下是执行这一操作的基本步骤:

  • 在跨平台库中定义该行为:为此,我在定义方法的库中创建一个抽象类,以阅读词典文件。我还定义了一个属性,是我的 DictionaryReader 类的一个实例。类似如下输出:
public abstract class DictionaryReader
{
  public abstract Task<ObservableCollection<string>>
    ReadDictionaryAsync(string path);
  public static DictionaryReader Instance { get; set; }
}
  • 在特定于平台的代码中实现该行为:为此,我从 Windows Phone Silverlight 项目中的 DictionaryReader 类中派生出此行为。我提供一个将词典文件作为应用资源加载和读取的 ReadDictionaryAsync 方法实现。请注意,此代码本质上与我的库中的代码相同,其中在资源路径上包含一些错误检查,因为我需要我的手机代码以特定的格式工作。我针对其他平台的实现将有所不同,具体取决于读取这些平台的应用本地资源的技术。但是,通过我添加的抽象(如图 6 中所示),这应该不成问题。
  • 将库定义的实例初始化到特定于平台的实现:为此,我会向 Windows Phone 项目的 App 类添加代码。此代码会将 DictionaryReader 的实例初始化到将文件作为资源读取的特定于手机的实现。同样,此代码位于我特定于平台的项目中,而不是我分析的项目中:
public App()
{
  DictionaryReader.Instance = new DictionaryReaderPhone();
...
}

图 6 加载和读取词典的手机实现

class DictionaryReaderPhone : DictionaryReader
{
  public override async Task<ObservableCollection<string>>
  ReadDictionaryAsync(string resourcePath)
{
  ObservableCollection<string> wordList =
    new ObservableCollection<string>();
  StreamResourceInfo info =
    Application.GetResourceStream(new Uri(resourcePath,
    UriKind.Relative));
  if (info == null)
    throw new ArgumentException("Could not load resource: " +
      resourcePath +
      ". For Windows Phone this should be in the
      Format:[project];component/[filename]");
  else
  {
    StreamReader reader = new StreamReader(info.Stream);
    string line;
    while ((line = await reader.ReadLineAsync()) != null)
    {
    wordList.Add(line);
    }
    reader.Close();
    return wordList;
    }
  }
}

对于显示如何抽象化特定于平台的代码的其他示例,请访问 aka.ms/q4sq7l,参阅 MSDN 库文章“使用可移植类库进行平台抽象”。

使用不同的 API 接下来,我会评估该条目,以获取当前区域性的名称。我双击该错误消息,并发现此方法:

public string CheckLanguage()
{
  return Thread.CurrentThread.CurrentCulture.Name.Split('-')[0];
}

根据 Portability Analyzer,我可以使用 CultureInfo.CurrentCulture。由于 CurrentCulture 是 CultureInfo 的静态属性并且提供与我从当前线程中获取的相同的信息,因此它可以正常工作。我使用以下代码(使用 CultureInfo)替换依赖获取 Thread 类的代码:

public string CheckLanguage()
{
  return System.Globalization.CultureInfo.CurrentCulture.Name.Split('-')[0];
}

到目前为止一切顺利

我测试所做的更改,并且一切看上去都很正常。接下来,我重新运行 Portability Analyzer。我的报告非常干净,其中只更改了少量代码,并且除了最初的平台目标之外,我还获得了 Xamarin.Android 的支持(如图 7 中所示)。

在代码更改后重新运行分析报告
图 7 在代码更改后重新运行分析报告

跨平台转换的最后一步是将特定于手机的库项目中的库项目转换为我可以从多个应用中引用的 PCL。最初,我认为执行这一步骤的最简单、最快捷的方法是通过手动修改项目文件来执行“就地”转换。

做了一些研究并尝试我发现的一些步骤之后,我得出的结论是,手动修改项目文件没有明显优势。我仔细地与 .NET 团队的一位成员进行了核实,他也这样认为。您会发现手动修改项目的最大帮助在于假设某个特定的起点。

根据您的项目历史记录和您已经进行的所有其他升级,您最终可能得到一个损坏的项目。花了大把时间尝试手动转换之后,很可能您需要创建新的 PCL 项目。此外,即使手动转换的项目最初可以正常使用,但如果以后进行其他修改,它可能会损坏。

因此,我创建了新的 PCL 项目,并且建议您也这样做。我将我的代码文件复制到新项目。复制代码后,我遇到了几个编译错误,这是因为使用了不再适用于新 PCL 的语句。我删除了这些语句,然后我的库可以顺利使用了。我将我的 Windows Phone Silverlight 应用切换为引用新的 PCL,而且我的应用再次正常运行了。

欢迎试用

使用 Portability Analyzer 不仅帮助我快速地评估我需要做哪些工作才能使我的库可以跨平台使用,而且还认识到从我的代码到方法调用和属性使用中的所有特定于平台的问题。它还建议使用哪些备用 API(若适用)。

我还向您展示了分解特定于平台的代码的技术。新的 Portability Analyzer 结果表示我的库将在我想要面向的其他平台以及 Xamarin.Android 上运行。最后,我能够使用新的目标创建新的 PCL 并从现有应用中引用此 PCL。新的 .NET Portability Analyzer 可帮助您将您的库用于新的平台。


10 年来,Cheryl Simmons 一直是 Microsoft 的程序员并且编写过 .NET 基类库、Windows 窗体、Windows Presentation Foundation、Silverlight、Windows Phone 和相关工具。

衷心感谢以下 Microsoft 技术专家对本文的审阅:Matt Galbraith、Daniel Plaisted 和 Taylor Southwick