65.9K
CodeProject 正在变化。 阅读更多。
Home

使用基于 Roslyn 的 VS 扩展实现枚举继承

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.93/5 (21投票s)

2015 年 2 月 22 日

CPOL

6分钟阅读

viewsIcon

42079

downloadIcon

360

描述用于生成子枚举(类似于子类)的 VS2015 扩展

重要提示

如果您能留下评论,说明您认为这篇文章可以如何改进,我将不胜感激。谢谢。

引言

在我的 C# 编程经验中,我遇到过许多情况,扩展一个简单的枚举会有所帮助。最常见的情况是,我需要使用一个我无法修改的 DLL 库中的枚举,同时,我还需要使用该库未包含的一些额外的枚举值。

Sergey Kryukov 在 Enumeration Types do not Enumerate! Working around .NET and Language Limitations(参见 2.5 节 Mocking Programming by Extension)中提出了一个类似的想法。

在这里,我尝试使用基于 Roslyn 的 VS 扩展来生成单个文件来解决这个问题,这类似于在 Implementing Adapter Pattern and Imitating Multiple Inheritance in C# using Roslyn based VS Extension Wrapper Generator 和后续文章中模拟多重继承的方法。

我使用的是 Visual Studio 2017,生成的 VSIX 文件应该只在 Visual Studio 2017 中有效。

阐述问题

请看 EnumDerivationSample 项目。它包含大量我们稍后会展示可以生成的非生成代码。该项目包含 BaseEnum 枚举

public enum BaseEnum
{
    A,
    B
} 

它还包含 DerivedEnum 枚举

public enum DerivedEnum
{
    A,
    B,
    C,
    D,
    E
}  

请注意,在 DerivedEnum 枚举中,枚举值 ABBaseEnum 枚举中的名称和整数值相同。

文件 DerivedEnum.cs 还包含一个 static DeriveEnumExtensions 类,该类具有从 BaseEnumDerivedEnum 和反之的转换扩展方法

public static class DeriveEnumExtensions
{
    public static BaseEnum ToBaseEnum(this DerivedEnum derivedEnum)
    {
        int intDerivedVal = (int)derivedEnum;

        string derivedEnumTypeName = typeof(DerivedEnum).Name;
        string baseEnumTypeName = typeof(BaseEnum).Name;
        if (intDerivedVal > 1)
        {
            throw new Exception
            (
                "Cannot convert " + derivedEnumTypeName + "." +
                derivedEnum + " value to " + baseEnumTypeName +
                " type, since its integer value " +
                intDerivedVal + " is greater than the max value 1 of " +
                baseEnumTypeName + " enumeration."
            );
        }

        BaseEnum baseEnum = (BaseEnum)intDerivedVal;

        return baseEnum;
    }

    public static DerivedEnum ToDerivedEnum(this BaseEnum baseEnum)
    {
        int intBaseVal = (int)baseEnum;

        DerivedEnum derivedEnum = (DerivedEnum)intBaseVal;

        return derivedEnum;
    }
}  

正如您所见,将 BaseEnum 值转换为 DerivedEnum 值始终成功,而反向转换可能会引发异常,如果 DerivedEnum 值大于 1(这是 BaseEnum.B 的值 - BaseEnum 枚举的最高值)。

Program.Main(...) 函数用于测试功能

static void Main(string[] args)
{
    // convert from based to derived value
    DerivedEnum derivedEnumConvertedValue = BaseEnum.A.ToDerivedEnum();
    Console.WriteLine("Derived converted value is " + derivedEnumConvertedValue);

    // convert from derived to base value
    BaseEnum baseEnumConvertedValue = DerivedEnum.B.ToBaseEnum();
    Console.WriteLine("Derived converted value is " + baseEnumConvertedValue);

    // throw a conversion exception trying to convert from derived to 
    // base value, because such value does not exist in the base enumeration:
    DerivedEnum.C.ToBaseEnum();
}  

它将打印

Derived converted value is A
Base converted value is B  

然后它将抛出一个包含以下消息的异常

 "Cannot convert DerivedEnum.C value to BaseEnum type, 
  since its integer value 2 is greater than the max value 1 of BaseEnum enumeration."

使用 Visual Studio 扩展生成枚举继承

现在,请通过双击文件从 VSIX 文件夹安装 NP.DeriveEnum.vsix Visual Studio 扩展。

打开 EnumDerivationWithCodeGenerationTest 项目。它的基枚举与前一个项目相同

public enum BaseEnum
{
    A,
    B
}  

请看文件 "DerivedEnum.cs"

[DeriveEnum(typeof(BaseEnum), "DerivedEnum")]
enum _DerivedEnum
{
    C,
    D,
    E
}  

它定义了一个带有属性的枚举 _DerivedEnum[DeriveEnum(typeof(BaseEnum), "DerivedEnum")]。该属性指定了“超枚举”(BaseEnum)和派生枚举的名称("DerivedEnum")。请注意,由于 C# 不支持 partial 枚举,我们被迫创建一个新的枚举,它结合了“超”和“子”枚举的值。

如果您查看 DerivedEnum.cs 文件的属性,您会发现它的“**Custom Tool**”属性设置为“DeriveEnumGenerator”值

现在,在 Visual Studio 中打开 DerivedEnum.cs 文件并尝试修改它(例如,添加一个空格)并保存。您会立即看到文件 DerivedEnum.extension.cs 被创建

此文件包含 DerivedEnum 枚举,它结合了 BaseEnum_DerivedEnum 枚举的所有字段,确保它们与原始枚举中的对应字段具有相同的名称和整数值

public enum DerivedEnum
{
    A,
    B,
    C,
    D,
    E,
} 

VS 扩展还生成了一个 staticDerivedEnumExtensions,其中包含子枚举和超枚举之间的转换方法

static public class DerivedEnumExtensions
{
    public static BaseEnum ToBaseEnum(this DerivedEnum fromEnum)
    {
        int val = ((int)(fromEnum));
        string exceptionMessage = "Cannot convert DerivedEnum.{0} 
                                   value to BaseEnum - there is no matching value";
        if ((val > 1))
        {
            throw new System.Exception(string.Format(exceptionMessage, fromEnum));
        }
        BaseEnum result = ((BaseEnum)(val));
        return result;
    }

    public static DerivedEnum ToDerivedEnum(this BaseEnum fromEnum)
    {
        int val = ((int)(fromEnum));
        DerivedEnum result = ((DerivedEnum)(val));
        return result;
    }
}  

现在,如果我们使用与上一个示例相同的 Program.Main(...) 方法,我们将获得非常相似的结果

static void Main(string[] args)
{
    // convert from based to derived value
    DerivedEnum derivedEnumConvertedValue = BaseEnum.A.ToDerivedEnum();
    Console.WriteLine("Derived converted value is " + derivedEnumConvertedValue);

    // convert from derived to base value
    BaseEnum baseEnumConvertedValue = DerivedEnum.B.ToBaseEnum();
    Console.WriteLine("Base converted value is " + baseEnumConvertedValue);

    // throw a conversion exception trying to convert from derived to 
    // base value, because such value does not exist in the base enumeration:
    DerivedEnum.C.ToBaseEnum();
}  

您可以尝试在子枚举和超枚举中指定字段值。代码生成器足够智能,能够生成正确的代码。例如,如果我们设置 BaseEnum.B20

public enum BaseEnum
{
    A,
    B = 20
}  

并将 _DerivedEnum.C 设置为 22

enum _DerivedEnum
{
    C = 22,
    D,
    E
}  

我们将得到以下生成的代码

public enum DerivedEnum
{

    A,
    B = 20,
    C = 22,
    D,
    E,
}  

ToBaseEnum(...) 扩展方法也将被更新,仅当我们尝试转换的 DerivedEnum 字段的整数值大于 20 时才抛出异常

public static BaseEnum ToBaseEnum(this DerivedEnum fromEnum)
{
    int val = ((int)(fromEnum));
    string exceptionMessage = "Cannot convert DerivedEnum.{0} 
                               value to BaseEnum - there is no matching value";
    if ((val > 20))
    {
        throw new System.Exception(string.Format(exceptionMessage, fromEnum));
    }
    BaseEnum result = ((BaseEnum)(val));
    return result;
}  

请注意,如果将子枚举的第一个字段更改为小于或等于超枚举的最后一个字段,则不会进行生成,此条件将报告为错误。例如,尝试将 _DerivedEnum.C 更改为 20 并保存更改。文件 DerivedEnum.extension.cs 将消失,您将在错误列表中看到以下错误

关于代码生成器实现的说明

实现代码生成的代码位于 NP.DeriveEnum 项目下。主项目 NP.DeriveEnum 是使用“**Visual Studio Package**”模板创建的(正如在 Implementing Adapter Pattern and Imitating Multiple Inheritance in C# using Roslyn based VS Extension Wrapper Generator 中所做的那样)。

我还必须添加 Roslyn 和 MEF2 包,以便通过从“NuGet 包管理器控制台”运行以下命令来使用 Roslyn 功能

  Install-Package Microsoft.CodeAnalysis -Pre
  Install-Package Microsoft.Composition

生成器的“main”类称为 DeriveEnumGenerator。它实现了 IVsSingleFileGenerator 接口。该接口有两个方法 - DefaultExtension(...)Generate(...)

方法 DefaultExtension(...) 允许开发人员指定生成的文件名的扩展名

public int DefaultExtension(out string pbstrDefaultExtension)
{
    pbstrDefaultExtension = ".extension.cs";

    return VSConstants.S_OK;
}  

方法 Generate(...) 允许开发人员指定进入生成文件的代码

public int Generate
(
    string wszInputFilePath,
    string bstrInputFileContents,
    string wszDefaultNamespace,
    IntPtr[] rgbOutputFileContents,
    out uint pcbOutput,
    IVsGeneratorProgress pGenerateProgress
)
{
    byte[] codeBytes = null;

    try
    {
        // generate the code
        codeBytes = GenerateCodeBytes(wszInputFilePath, bstrInputFileContents, 
                                      wszDefaultNamespace);
    }
    catch(Exception e)
    {
        // add the error to the "Error List"
        pGenerateProgress.GeneratorError(0, 0, e.Message, 0, 0);
        pcbOutput = 0;
        return VSConstants.E_FAIL;
    }
    int outputLength = codeBytes.Length;
    rgbOutputFileContents[0] = Marshal.AllocCoTaskMem(outputLength);
    Marshal.Copy(codeBytes, 0, rgbOutputFileContents[0], outputLength);
    pcbOutput = (uint)outputLength;

    return VSConstants.S_OK;
}  

在我们的例子中,代码生成实际上是由 Generate(...) 方法调用的 GenerateCodeBytes(...) 方法完成的。

protected byte[] GenerateCodeBytes
(string filePath, string inputFileContent, string namespaceName)
{
    // set generatedCode to empty string
    string generatedCode = "";

    // get the id of the .cs file for which we are 
    // trying to generate code based on the class'es DeriveEnum attribute
    DocumentId docId =
        TheWorkspace
            .CurrentSolution
            .GetDocumentIdsWithFilePath(filePath).FirstOrDefault();

    if (docId == null)
        goto returnLabel;

    // get the project that contains the file for which 
    // we are generating the code.
    Project project = TheWorkspace.CurrentSolution.GetProject(docId.ProjectId);
    if (project == null)
        goto returnLabel;

    // get the compilation of the project. 
    Compilation compilation = project.GetCompilationAsync().Result;

    if (compilation == null)
        goto returnLabel;

    // get the document based on which we 
    // generate the code
    Document doc = project.GetDocument(docId);

    if (doc == null)
        goto returnLabel;

    // get the Roslyn syntax tree of the document
    SyntaxTree docSyntaxTree = doc.GetSyntaxTreeAsync().Result;
    if (docSyntaxTree == null)
        goto returnLabel;

    // get the Roslyn semantic model for the document
    SemanticModel semanticModel = compilation.GetSemanticModel(docSyntaxTree);
    if (semanticModel == null)
        goto returnLabel;

    // get the document's class node
    // Note that we assume that the top class within the 
    // file is the one that we want to generate the wrappers for
    // It is better to make it the only class within the file. 
    EnumDeclarationSyntax enumNode =
        docSyntaxTree.GetRoot()
            .DescendantNodes()
            .Where((node) => (node.CSharpKind() == 
             SyntaxKind.EnumDeclaration)).FirstOrDefault() as EnumDeclarationSyntax;

    if (enumNode == null)
        goto returnLabel;

    // get the enum type.
    INamedTypeSymbol enumSymbol = 
          semanticModel.GetDeclaredSymbol(enumNode) as INamedTypeSymbol;
    if (enumSymbol == null)
        goto returnLabel;

    // get the generated code
    generatedCode = enumSymbol.CreateEnumExtensionCode();

    returnLabel:
    byte[] bytes = Encoding.UTF8.GetBytes(generatedCode);

    return bytes;
}

Generate(...) 方法将 C# 文件的路径作为参数之一。我们使用该路径来获取我们正在处理的文档的 Roslyn 文档 ID

// get the id of the .cs file for which we are 
// trying to generate wrappers based on the class'es Wrapper Attributes
DocumentId docId =
    TheWorkspace
        .CurrentSolution
        .GetDocumentIdsWithFilePath(filePath).FirstOrDefault();

从文档 ID,我们可以通过使用 dockId.ProjectId 属性来获取项目 ID。

从项目 ID,我们从 Roslyn Workspace 获取 Roslyn Project

Project project = TheWorkspace.CurrentSolution.GetProject(docId.ProjectId);  

并从 Project 获取其编译

Compilation compilation = project.GetCompilationAsync().Result;  

我们还从项目中获取 Roslyn Document

 Document doc = project.GetDocument(docId);  

从当前文档,我们获取其 Roslyn SyntaxTree

SyntaxTree docSyntaxTree = doc.GetSyntaxTreeAsync().Result;

从 Roslyn CompilationSyntaxTree,我们得到语义模型

SemanticModel semanticModel = compilation.GetSemanticModel(docSyntaxTree);  

我们还从 SyntaxTree 获取文件中声明的第一个枚举语法

EnumDeclarationSyntax enumNode =
    docSyntaxTree.GetRoot()
        .DescendantNodes()
        .Where((node) => (node.CSharpKind() == 
         SyntaxKind.EnumDeclaration)).FirstOrDefault() as EnumDeclarationSyntax;

最后,从 SemanticModelEnumerationDeclarationSyntax,我们可以提取与枚举对应的 INamedTypeSymbol

INamedTypeSymbol enumSymbol = semanticModel.GetDeclaredSymbol(enumNode) as INamedTypeSymbol;  

正如我在前几篇 Roslyn 相关文章中提到的,INamedTypeSymbol 非常类似于 System.Reflection.Type。您可以从 INamedTypeSymbol 对象中获取有关 C# 类型的几乎所有信息。

扩展方法 DOMCodeGenerator.CreateEnumExtensionCode() 生成并返回所有代码

generatedCode = enumSymbol.CreateEnumExtensionCode();  

负责代码生成的其余代码位于 NP.DOMGenerator 项目中。

正如我之前提到的——我只使用 Roslyn 进行分析——对于代码生成,我使用的是 CodeDOM,因为它更简洁,更有意义。

NP.DOMGenerator 项目下有两个主要的 static 类 - RoslynExtensions - 用于 Roslyn 分析,DOMCodeGenerator - 用于使用 CodeDOM 功能生成代码。

结论

在本文中,我们描述了创建 VS 2017 扩展以生成子枚举(类似于子类)。

历史

  • 2015 年 2 月 22 日:初始版本
  • 2017 年 11 月 14 日:修改代码以在 Visual Studio 2017 下使用最新的 Roslyn 库运行
© . All rights reserved.