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

基于 Roslyn 的模拟多重继承用法模式(第一部分)

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.76/5 (6投票s)

2014 年 12 月 26 日

CPOL

13分钟阅读

viewsIcon

25176

downloadIcon

522

介绍如何使用 VS 2015 预览版中基于 Roslyn 的扩展来模拟 C# 中的多重继承,并提供使用示例。

重要提示

朋友们,如果您喜欢或不喜欢这篇文章,请留下评论,我将不胜感激。

引言

使用基于 Roslyn 的 VS 扩展在 C# 中实现适配器模式和模拟多重继承 一文中,我介绍了一种使用基于 Roslyn 的单文件生成器在 C# 中模拟多重继承的方法。这种模拟多重继承的方法基于 C# 的模拟多重继承模式 文章中描述的方法,不同之处在于“子类”包装器是使用该文章中介绍的 Visual Studio 扩展自动生成的。

我早就有了通过代码生成实现多重继承的想法,但直到 Roslyn 才得以实现。在 Roslyn 出现之前,没有可用的工具来分析代码并提取 Visual Studio 扩展中进行包装器生成所需的所有信息——当然,也存在一些专有解决方案,例如 ReSharper,但它们并非人人可用。然而,有了 Roslyn,其可以实现的潜力是无限的。我的包装器生成器可能仅仅是即将到来的更强大功能的冰山一角。

我上一篇文章的几位评论者对我说将生成的模式称为“模拟多重继承”感到不满。他们认为生成的结构不是多态的。

本文的一个目标正是展示如何通过生成的代码并借助接口来实现多重继承的多态性。

本文的主要目的是展示如何通过使用 Roslyn 生成的多重继承和适配器模式来改进关注点分离和代码重用。

Roslyn 与 Visual Studio 的集成仅包含在 VS 2015 预览版中,因此您需要使用此版本来运行本文的示例。

上次我尝试一次上传所有代码,但 CodeProject 不允许,所以现在我将源代码分成更小的块——一个项目一个项目地上传。

Visual Studio 扩展 NP.WrapperGenerator.vsix 的功能已得到扩展,增加了另一种类型的事件包装(我称之为单向事件包装),这将在文章后面进行详细解释。

在本期中,我将回顾包装器生成,讨论使用接口实现多态“多重继承”,并描述单向事件包装。

在本文的下一期中,我计划提供更多引人注目的基于 WPF 的多态多重继承示例,并提供一个使用 virtual 关键字在 C++ 中实现的(臭名昭著的)菱形继承的示例。

安装 Visual Studio 扩展 NP.WrapperGenerator.vsix

为了能够完成下面的示例,您需要安装 VSIS.zip 文件中的 NP.WrapperGenerator.vsix 扩展。安装它所要做的就是解压缩文件夹并双击扩展文件。之后,您需要重启您的 VS 2015 实例,以便它们能够使用新扩展。

如果您玩过上一篇文章的示例,您可能已经安装了此扩展的旧版本。在这种情况下,您需要在安装新版本之前卸载它。为此,请在您的 VS 2015 的任何实例中转到“工具 -> 扩展和更新”菜单项。找到 WrapperGenerator 扩展(通常在底部),单击它,然后按“卸载”按钮。

回顾

在此,我简要回顾一下上一篇文章 使用基于 Roslyn 的 VS 扩展在 C# 中实现适配器模式和模拟多重继承,以便读者能够独立阅读本文。

包装器生成和继承类比的主要原则

如上一篇文章所述,NP.WrapperGenerator.vsix 扩展可用于生成一个部分类文件,其中包含对其他类的属性、事件和方法的包装器。我们使用类属性来指定应生成哪些包装器,并且包含这些属性的文件的“自定义工具”属性应设置为“WrapperFileCodeGenerator”。

类属性还允许更改被包装类成员的名称和封装级别,例如,公共属性 Name 可以包装为受保护属性 PersonName

包装器生成示例

此示例的目的是演示 NP.WrapperGenerator.vsix 扩展的功能。

示例解决方案位于 WrapperGenerationSample 文件夹下。

要包装的类名为 Person。它是一个非常简单的类,包含两个属性:NameAge,一个事件 NameChangedEvent 和一个方法 - void ChangeName(string newName)

public class Person
{
    string _name;
    public string Name
    {
        get
        {
            return _name;
        }

        set
        {
            _name = value;

            if (NameChangedEvent != null)
                NameChangedEvent(this);
        }
    }

    public int Age { get; set; }

    public event Action NameChangedEvent = null;

    public void ChangeName(string newName)
    {
        Name = newName;
    }
}  

包装 Person 的类名为 PersonWrapper。它被定义为部分类,并具有一些特殊的类属性,这些属性指定了包装。

[Wraps(typeof(Person), WrappedItemsKind.Property, "Name", "Age")]
[Wraps(typeof(Person), WrappedItemsKind.Event, "NameChangedEvent")]
[WrapsAndChanges(typeof(Person), WrappedItemsKind.Method, "ChangeName", "ChangePersonName", EncapsulationLevel.Internal)]
public partial class PersonWrapper
{
}  

为了进行包装器生成,“Custom Tool”属性的 PersonWrapper.cs 文件应设置为“WrapperFileCodeGenerator”。

一旦设置了“Custom Tool”属性,或者一旦 PersonWrapper.cs 文件被修改并保存,NP.WrapperGenerator 扩展就会生成一个依赖文件 PersonWrapper.wrapper.cs,其中包含此部分类的另一部分,即包装器。

这是 PersonWrapper.wrapper.cs 文件的内容。

using System;
using WrapperGenerationSample;


namespace WrapperGenerationSample
{
    
    
    public partial class PersonWrapper
    {
        
        private Person _person;
        
        public static implicit operator Person (PersonWrapper objectToConvert)
                                   {
                                       return objectToConvert._person;
                                   }
                         
        public event Action NameChangedEvent
            { 
                add { _person.NameChangedEvent += value; } 
                remove { _person.NameChangedEvent -= value; } 
            }
        
        public Person ThePerson
        {
            get
            {
                return _person;
            }
            set
            {
                _person = value;
            }
        }
        
        public String Name
        {
            get
            {
                return _person.Name;
            }
            set
            {
                _person.Name = value;
            }
        }
        
        public Int32 Age
        {
            get
            {
                return _person.Age;
            }
            set
            {
                _person.Age = value;
            }
        }
        
        internal void ChangePersonName(String newName)
        {
            _person.ChangeName(newName);
        }
    }
}  

首先,它包含一个类型为 Person 的公共属性 ThePerson

private Person _person;
...
public Person ThePerson
{
    get
    {
        return _person;
    }
    set
    {
        _person = value;
    }
}  

此属性代表要包装的对象。ThePerson 对象可以在 PersonWrapper 类的构造函数中设置——用继承的语言来说,ThePerson 对象代表 PersonWrapper基类,并在 PersonWrapper 的构造函数中设置它,这相当于调用基类的 base(...) 构造函数。

PersonWrapper.wrapper.cs 类的其余功能仅包含对 ThePerson 对象属性、事件和方法的包装,但有一个重要的例外:隐式转换运算符。

public static implicit operator Person (PersonWrapper objectToConvert)
                           {
                               return objectToConvert._person;
                           }  

它通过简单地返回 ThePerson 属性引用的对象,将 PersonWrapper 对象转换为 Person。这相当于子类到超类的隐式转换。

现在,看看 PersonWrapper 类上方的类属性。(这些属性,顺便说一句,需要对位于 WrapperGenerationSample/Dlls 文件夹中的 NP.WrapperAttrs.dll 文件进行引用)。

Wraps 属性需要被包装类的类型(超类)、要包装的类成员的种类 - 属性、事件或方法,以及该种类下要包装的类成员的名称列表,例如,在我们的例子中,它可以是“Name”和“Age”属性,或“NameChangedEvent”事件。

[Wraps(typeof(Person), WrappedItemsKind.Property, "Name", "Age")]
[Wraps(typeof(Person), WrappedItemsKind.Event, "NameChangedEvent")]  

WrapsAndChanges 属性功能更强大——它允许更改类成员的名称和封装级别,但它一次只能处理一个类成员——您不能像上面使用“Name”和“Age”那样一次传递多个成员。

[WrapsAndChanges(typeof(Person), WrappedItemsKind.Method, "ChangeName", "ChangePersonName", EncapsulationLevel.Internal)]  

此属性将 ChangeName(...) 方法的名称更改为 ChangePersonName(...),并将包装器方法的封装级别设置为 internal。顺便说一句,如果未指定封装级别,它将与被包装的类成员相同——不一定是 public

可以在 Program.cs 文件中找到 PersonWrapper 类的使用示例。

static void Main(string[] args)
{
    PersonWrapper personWrapper = new PersonWrapper
    {
        // within the constructor set the 'base' object
        ThePerson = new Person { Name = "John", Age = 30 }
    };

    // set the event handler
    personWrapper.NameChangedEvent += PersonWrapper_NameChangedEvent;

    // change the name and make sure that the handler prints the new name
    personWrapper.ChangePersonName("Johnathan");
}

private static void PersonWrapper_NameChangedEvent(Person obj)
{
    Console.WriteLine(obj.Name);
}  

关于继承和多重继承的讨论

我上一篇文章的一些评论者认为上面介绍的包装器生成与继承无关。

事实上,这正是编译器实现类继承的方式——它们创建超类对象并在其周围提供包装器,只是它们是在二进制代码中完成的,而我不得不进行代码生成。

其他读者担心生成的“子类”不是多态的——即它们不能用作“超类”的替代品。然而,我们可以使用接口来解决这个问题,如下面将要展示的。

但是,这种类型的继承存在一些限制。

  1. 这种类型的继承不能在“子类”中重写“超类”中使用的虚函数。C# 不允许我更改函数指针,因此创建虚函数将需要更具侵入性的二进制代码生成方法。
  2. 以及一个相关的问题是,这种继承不允许在“超类”中使用正确的 this 指针——“超类”中的 this 将是被包装的对象,而不是包装器对象。当涉及到属性或方法时,这并不是一个大问题——因为“超类”的属性和方法不应该了解“子类”。但当涉及到包含 this 指针作为参数的事件时,这可能会造成很大的问题。下面描述的“单向”事件包装将展示如何绕过这个问题。

 

总的来说,尽管存在这两个限制,这种类型的继承仍然非常有用,正如我希望在下面向您证明的那样。

在上面的“回顾”示例中,我们让 PersonWrapper 类“继承”自 Person 类——这是单一类继承。然而,没有什么能阻止我们使用多个“基类”从而模仿多重继承,正如下面的示例将要展示的那样。

简单的带多态的多重继承示例

此示例的目的是展示如何使用我们的继承和接口来实现多态。

示例代码位于 SimplePolymorphism 解决方案下。

主项目包含两个非常简单的接口 IPersonISelectable

public interface IPerson
{
    string Name { get; set; }

    void PrintInfoToConsole();
}
public interface ISelectable
{
    bool IsItemSelected { get; set; }

    event Action ItemSelectedChangedEvent;
}  

这两个接口有两个(同样非常简单的)实现:分别是 PersonSelectable。特别是 PersonPrintInfoToConsol() 方法的实现,它会打印一行“Name =””,其中“”应替换为 Person 对象的 Name 属性。

public void PrintInfoToConsole()
{
    Console.WriteLine("Name = " + Name);
}  

Program 类的静态方法 Program.PrintPersonInfoToConsol(IPerson person) 调用传递给它的 IPerson 对象的 PrintInfoToConsole() 方法。

static void PrintPersonInfoToConsole(IPerson person)
{
    person.PrintInfoToConsole();
}  

这是包装器类 SelectablePerson 的定义(在我们看来,它扩展了 PersonSelectable 类)。

[Wraps(typeof(Selectable), WrappedItemsKind.Event, "ItemSelectedChangedEvent")]
[Wraps(typeof(Selectable), WrappedItemsKind.Property, "IsItemSelected")]
[Wraps(typeof(Selectable), WrappedItemsKind.Method, "ToggleIsSelected")]
[Wraps(typeof(Person), WrappedItemsKind.Property, "Name")]
[Wraps(typeof(Person), WrappedItemsKind.Method, "PrintInfoToConsole")]
public partial class SelectablePerson : IPerson, ISelectable
{

}  

正如您所见,生成的包装器成员也使其实现了 IPersonISelectable 接口。作为 IPerson 对象,此类的实例可以传递给 Program.PrintPersonInfoToConsol(IPerson person) 方法。

看看 Program 类——我们创建了一个 SelectablePerson 对象并将其传递给 Program.PrintPersonInfoToConsol(IPerson person) 方法。

SelectablePerson selectablePerson = new SelectablePerson
{
    ThePerson = new Person { Name = "Joe" },
    TheSelectable = new Selectable()
};

PrintPersonInfoToConsole(selectablePerson);  

正如预期的那样,当运行上面的代码时,将在控制台打印“Name = Joe”。

如果我们想创建另一个实现 IPersonISelectable 的类,但其 PrintInfoToConsole() 方法的实现不同——比如说,它打印“Hello Joe”而不是打印“Name = Joe”。SelectablePersonWithModifiedMethod 类展示了如何做到这一点。

[Wraps(typeof(Selectable), WrappedItemsKind.Event, "ItemSelectedChangedEvent")]
[Wraps(typeof(Selectable), WrappedItemsKind.Property, "IsItemSelected")]
[Wraps(typeof(Selectable), WrappedItemsKind.Method, "ToggleIsSelected")]
[Wraps(typeof(Person), WrappedItemsKind.Property, "Name")]
public partial class SelectablePersonWithModifiedMethod : IPerson, ISelectable
{
    public void PrintInfoToConsole()
    {
        Console.WriteLine("Hello " + this.Name);
    }
}  

我们没有为 PrintInfoToConsole() 方法设置 Wraps 属性,而是显式提供了它的实现。

创建这样一个对象并将其传递给 Program.PrintPersonInfoToConsol 方法将打印“Hello Joe”。

SelectablePersonWithModifiedMethod selectablePersonWithModifiedMethod = new SelectablePersonWithModifiedMethod
{
    ThePerson = new Person { Name = "Joe" },
    TheSelectable = new Selectable()
};

PrintPersonInfoToConsole(selectablePersonWithModifiedMethod); 

正如您所见——接口多态性得以保留——在以我们方式继承的对象上调用了正确的函数。

单向事件包装器生成与 this 引用替换

对于熟悉 WPF 的人来说,C# 的一个最不光彩的例子是它无法以通用方式将与在视图模型中定义和触发 PropertyChanged 事件相关的代码提取出来。当然,您可以定义一个 PropertyChangedImpl 类,如下所示:

public class PropertyChangedImpl : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    public void OnPropertyChanged(string propName)
    {
        if (PropertyChanged == null)
            return;

        PropertyChanged(this, new PropertyChangedEventArgs(propName));
    }
}  

然后,您所有的视图模型都可以继承自 PropertyChangedImpl 类。但是,由于 C# 中缺乏多重继承,继承 PropertyChangedImpl 类将阻止您继承任何其他类,因此,许多时候您会认为这不值得,并且会简单地复制粘贴 PropertyChangedImpl 代码。

在本节中,我将描述如何通过包装器生成继承来缓解这个问题,并描述我在弄清楚如何使 PropertyChangedImpl 继承工作时遇到的一个新概念——单向事件包装器生成。

看看 PropertyChnagedWrapperSample 项目。它的视图模型 MyViewModel 类只包含一个属性 TheText,它调用方法 OnPropertyChanged,该方法是对 PropertyChangedImpl 类的同名方法的包装。

[Wraps(typeof(PropertyChangedImpl), WrappedItemsKind.Event, "PropertyChanged")]
[Wraps(typeof(PropertyChangedImpl), WrappedItemsKind.Method, "OnPropertyChanged")]
public partial class MyViewModel : INotifyPropertyChanged
{
    //public event PropertyChangedEventHandler PropertyChanged;

    public MyViewModel()
    {
        this.ThePropertyChangedImpl = new PropertyChangedImpl();

        //this.ThePropertyChangedImpl.PropertyChanged += ThePropertyChangedImpl_PropertyChanged;
    }

    //private void ThePropertyChangedImpl_PropertyChanged(object sender, PropertyChangedEventArgs e)
    //{
    //    if (PropertyChanged != null)
    //    {
    //        PropertyChanged(this, e);
    //    }
    //}

    string _text = null;
    public string TheText
    {
        get
        {
            return _text;
        }

        set
        {
            if (_text == value)
                return;

            _text = value;

            OnPropertyChanged("TheText");
        }
    }
}  

MainWindow.xaml 文件在其 Resources 部分定义了一个视图模型实例,并在上方定义了一个 TextBlock,在下方定义了一个 TextBoxTextBlockTextBoxText 属性都绑定到视图模型的 TheText 属性,因此,如果绑定工作正常并且 PropertyChanged 事件被正确触发,那么在 TextBox 中输入的任何内容都应该出现在上面的 TextBlock 中。


    


    
        

        
            
            
        
    
  

然而,如果我们运行项目,我们会注意到上面的 TextBlock 没有更新。

这里出错的是 PropertyChanged 事件的触发——特别是在处理传递给它的 this 引用时。确实,在 MyViewModel 类上调用的 OnPropertyChanged(...) 会调用被包装的 PropertyChangedImpl 对象的 OnPropertyChanged(...) 方法,该方法会触发该对象上的 PropertyChanged 事件,并将 this 引用传递给它,其中 this 指的是 PropertyChangedImpl 对象而不是 MyViewModel 包装器。

PropertyChanged(this, new PropertyChangedEventArgs(propName));  

因此,绑定——它设置在 MyViewModel 对象上——没有注意到 PropertyChanged 事件的触发。

不过,有一个方法可以解决这个问题:注释掉 PropertyChanged 事件的 Wrap 属性,并取消注释所有注释掉的行,使 MyViewModel 类看起来像这样:

using NP.WrapperAttrs;
using System.ComponentModel;


namespace PropertyChangedWrapperSample
{
    //[Wraps(typeof(PropertyChangedImpl), WrappedItemsKind.Event, "PropertyChanged")]
    [Wraps(typeof(PropertyChangedImpl), WrappedItemsKind.Method, "OnPropertyChanged")]
    public partial class MyViewModel : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;

        public MyViewModel()
        {
            this.ThePropertyChangedImpl = new PropertyChangedImpl();

            this.ThePropertyChangedImpl.PropertyChanged += ThePropertyChangedImpl_PropertyChanged;
        }

        private void ThePropertyChangedImpl_PropertyChanged(object sender, PropertyChangedEventArgs e)
        {
            if (PropertyChanged != null)
            {
                PropertyChanged(this, e);
            }
        }

        string _text = null;
        public string TheText
        {
            get
            {
                return _text;
            }

            set
            {
                if (_text == value)
                    return;

                _text = value;

                OnPropertyChanged("TheText");
            }
        }
    }
}  

做出这些更改后,再次尝试运行示例。现在一切正常,当您在 TextBox 中输入“Hello”时,您将在上方看到 Hello

让我们看看这里发生了什么。MyViewModel.PropertyChanged 事件不再是 PropertyChangedImpl.PropertyChanged 事件的完美包装器,而是我们定义了一个单独的实体,当 PropertyChangedImpl.PropertyChanged 事件触发时,它会被触发。此外,我们用 this 引用替换了事件的第一个参数。由于这种替换是在 MyViewModel 类内部完成的,因此将正确的对象传递给它。

public MyViewModel()
{
    this.ThePropertyChangedImpl = new PropertyChangedImpl();

    this.ThePropertyChangedImpl.PropertyChanged += ThePropertyChangedImpl_PropertyChanged;
}

private void ThePropertyChangedImpl_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
    if (PropertyChanged != null)
    {
        PropertyChanged(this, e);
    }
}  

这就是我所说的单向事件包装器,带有 this 引用替换(如果有人能想出更好、更简洁的名称——我很乐意考虑:-)))。

我在新版本的 NP.WrapperGenerator.vsix 扩展中添加了 OneWayEventWraps 属性和生成单向事件相关包装器的功能。

为了看到新属性的实际效果,请查看 PropertyChangedOneWayEventWrapSample 项目。它与上面考虑的 PropertyChnagedWrapperSample 项目非常相似。唯一的区别是,我们使用 OneWayEventWraps 扩展代替了 MyViewModel 类中注释掉的代码。这是 MyViewModel 类现在的样子:

[OneWayEventWraps(typeof(PropertyChangedImpl), "PropertyChanged", "sender")]
[Wraps(typeof(PropertyChangedImpl), WrappedItemsKind.Method, "OnPropertyChanged")]
public partial class MyViewModel : INotifyPropertyChanged
{
    public MyViewModel()
    {
        this.ThePropertyChangedImpl = new PropertyChangedImpl();
    }

    string _text = null;
    public string TheText
    {
        get
        {
            return _text;
        }

        set
        {
            if (_text == value)
                return;

            _text = value;

            OnPropertyChanged("TheText");
        }
    }
}  

OneWayEventWraps 属性将其第一个参数作为被包装对象(“基类”)。第二个参数是被包装事件的名称——在本例中是 PropertyChanged。第三个参数是可选的(仅当您想替换 this 引用时),它指定了事件委托的参数名称,该参数将被替换为 this 引用。在本例中,事件的委托类型为 void (object sender, PropertyChangedEventArgs e),因此我们想将其名为“sender”的参数替换为 this 引用。

这是 MyViewModel.wrapper.cs 文件生成的代码——我只显示与单向事件包装相关的部分(我还添加了一些注释来解释代码)。

...
// declare the PropertyChanged event within the wrapper class
public event PropertyChangedEventHandler PropertyChanged;

public PropertyChangedImpl ThePropertyChangedImpl
{
    get
    {
        return _propertyChangedImpl;
    }
    set
    {
        if ((_propertyChangedImpl != null))
        {
            // remove event handler to the old 'base' object's PropertyChanged event
            _propertyChangedImpl.PropertyChanged -= _propertyChangedImpl_PropertyChanged;
        }
        _propertyChangedImpl = value;
        if ((_propertyChangedImpl != null))
        {
            // added event handler to the new 'base' object's PropertyChanged event
            _propertyChangedImpl.PropertyChanged += _propertyChangedImpl_PropertyChanged;
        }
    }
}  

// the event handler
private void _propertyChangedImpl_PropertyChanged(Object sender, PropertyChangedEventArgs e)
{
    if ((PropertyChanged != null))
    {
        // replaced 'sender' object with 'this' reference.
        PropertyChanged(this, e);
    }
}
...

正如本文第二部分将要展示的那样——单向事件包装在 PropertyChanged 事件之外还有更多用途。

结论

在本文中,我们展示了一些简单的基于包装器的模拟多重继承的案例,并讨论了定义和展示的单向事件包装。

本文的下一部分将讨论更复杂的多重继承多态案例以及解决菱形多重继承问题。

© . All rights reserved.