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

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.93/5 (5投票s)

2014 年 12 月 28 日

CPOL

12分钟阅读

viewsIcon

27179

downloadIcon

573

继续介绍 NP.WrapperGenerator.vsix Visual Studio 扩展的用法。展示更复杂的多重继承场景,包括菱形多重继承。

重要提示

朋友们,我很希望能收到您关于本文档的评论,无论您喜欢还是不喜欢,我都非常感激。

引言

Visual Studio 2015 预览版附带了基于 Roslyn 的编译器,并且能够使用 Roslyn 创建 VS 扩展。这开启了以前从未存在过的无数机会,而这就是我尝试利用它的尝试。Roslyn 允许在不加载 .NET 代码的情况下对其进行分析——在此之前,这种能力仅存在于专有解决方案中,例如 ReSharper。

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

本文档与《基于 Roslyn 的模拟多重继承用法模式 (第一部分)》一起,展示了 NP.WrapperGenerator.vsix VS 扩展的用法示例。其主要目的是展示如何利用此扩展实现更大的代码重用和关注点分离。我还试图回应读者们的担忧,表明生成的结构确实非常类似于多重继承,并且借助接口,我们甚至可以实现多态多重继承。

与本文档的第一部分(基于 Roslyn 的模拟多重继承用法模式 (第一部分))不同,第二部分讨论了更有趣和更复杂的场景。

这是第一部分文章的续篇,因此建议您先阅读第一部分。

如果您想了解更多关于 NP.WrapperGenerator.vsix 扩展的构建方式,请参阅《C# 的模拟多重继承模式》文章。

在文章的最后,我们将展示如何处理菱形多重继承——即两个超类继承自同一个类。

要运行示例和安装扩展,您需要安装 VS 2015 预览版。

安装 Visual Studio 扩展 NP.WrapperGenerator.vsix

为了能够通过下面的示例进行操作,您需要安装包含在 VSIS.zip 文件中的 NP.WrapperGenerator.vsix 扩展。要安装它,只需解压文件夹并双击扩展文件即可。之后,您需要重新启动您的 VS 2015 实例,以便它们能够使用新扩展。

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

唯一项目选择示例

此示例位于 UniqueItemSelectionSample 解决方案下。此示例的想法来自我之前的一篇 WPF 文章——《基于 View-ViewModel 的 WPF 和 XAML 实现模式》。

该示例演示了如何构建一个非可视化的非可视项集合,该集合一次只允许选择一项——当选择一项时,之前选择的项将被取消选择。

对这些项唯一的约束是它们应该实现 ISelectable 接口

public interface ISelectable
{
    bool IsItemSelected { get; set; }

    event Action ItemSelectedChangedEvent;
}  

并且该接口的实现应该在 IsItemSelected 属性更改时触发 ItemSelectedChangedEvent

在 WPF 文章中,所有可选项都派生自 Selectable 类。在这里,我们展示了如何使用包装器生成来“派生”我们的类,使其成为 Selectable

首先,请尝试运行项目。您将看到以下内容

有两行不同的对象,顶行是人,底行是汽车。

拥有两行不同的原因是显示完全不同的类——PersonCar——可以使用我们的包装器“继承”来使它们成为 ISelectable,而无需真正的 C# 继承。

当您单击一个按钮时,它会被选中,并且同一行中先前选中的按钮会被取消选中。

现在,让我们看一下代码。这是 Selectable 类的实现

public class Selectable : ISelectable, INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    protected void OnPropChanged(string propName)
    {
        if (PropertyChanged != null)
            PropertyChanged(this, new PropertyChangedEventArgs(propName));
    }

    public event Action ItemSelectedChangedEvent;

    bool _isItemSelected = false;
    public bool IsItemSelected
    {
        get
        {
            return _isItemSelected;
        }
        set
        {
            if (_isItemSelected == value)
                return;

            _isItemSelected = value;

            if (ItemSelectedChangedEvent != null)
                ItemSelectedChangedEvent(this);

            OnPropChanged("IsItemSelected");
        }
    }

    public void ToggleIsSelected()
    {
        IsItemSelected = !IsItemSelected;
    }
}  

ItenSelectedChangedEvent 被设置为在 IsItemSelected 属性更改时触发。可选项对象本身作为唯一参数传递给它。此外,为了让 WPF 绑定注意到 IsItemSelected 属性的变化,我们使用 OnPropChanged(...) 方法触发 PropertyChanged 事件。

Person 类只有一个简单的属性——Name

public class Person
{
    public string Name { get; set; }
}  

Car 类有两个简单的属性——MakeModel

public class Car
{
    public string Make { get; set; }    
    public string Model { get; set; }
}  

SelectablePerson 类(在包装器的意义上)派生自 SelectablePerson

[OneWayEventWraps(typeof(Selectable), "PropertyChanged", "sender")]
[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 SelectablePerson : ISelectable, INotifyPropertyChanged
{
    public SelectablePerson(string name) 
    {
        this.TheSelectable = new Selectable();
        this.ThePerson = new Person { Name = name };
    }
}  

正如在《基于 Roslyn 的模拟多重继承用法模式 (第一部分)》中所解释的,这些属性指定了要包装的类型和成员——例如

  [Wraps(typeof(Selectable), WrappedItemsKind.Event, "ItemSelectedChangedEvent")]

包装了 Selectable 类的 ItemSelectedChangedEvent

实际上,SelectablePerson.wrapper.cs 部分类定义是生成的,其中包含包装器。(为了实现这一点——您需要将 SelectablePerson 类的“自定义工具”属性设置为字符串“WrapperFileCodeGenerator”——正如本文档的第一部分所述)。

Wraps 属性中提到 SelectablePerson 类型,导致在 SelectablePerson.wrapper.cs 文件中生成了相应的 Selectable 类型的属性 TheSelectablePerson 类型的属性 ThePerson

public Selectable TheSelectable
{
    get
    {
        return _selectable;
    }
    set
    {
        if ((_selectable != null))
        {
            _selectable.PropertyChanged -= _selectable_PropertyChanged;
        }
        _selectable = value;
        if ((_selectable != null))
        {
            _selectable.PropertyChanged += _selectable_PropertyChanged;
        }
    }
}

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

TheSelectable 属性的 setter 中设置 PropertyChanged 事件处理程序的代码将在下面解释。

TheSelectableThePerson 属性指定的 SelectablePerson 对象现在扮演着基类对象的作用。所有包装器都围绕着这些对象的成员构建。例如,这是 ItemSelectedChangedEvent 包装器的定义

public event Action ItemSelectedChangedEvent
    { 
        add { _selectable.ItemSelectedChangedEvent += value; } 
        remove { _selectable.ItemSelectedChangedEvent -= value; } 
    }  

这是我们定义 ToggleIsSelected 方法包装器的方式

public void ToggleIsSelected()
{
    _selectable.ToggleIsSelected();
}  

这是我们包装 Person 类的 Name 属性的方式

public String Name
{
    get
    {
        return _person.Name;
    }
    set
    {
        _person.Name = value;
    }
} 

现在,让我解释一下在 TheSelectable 属性定义中添加 PropertyChanged 事件处理程序中的代码。这段代码来自 OneWayEventWraps 属性。

  [OneWayEventWraps(typeof(Selectable), "PropertyChanged", "sender")]  

有关使用 this 引用替换的单向事件包装的详细说明,请参阅《基于 Roslyn 的模拟多重继承用法模式 (第一部分)》。

此属性会产生一种特殊的事件包装——我们不是定义事件的 addremove 部分,而是向“基”对象的事件添加一个事件处理程序。

if ((_selectable != null))
{
    _selectable.PropertyChanged += _selectable_PropertyChanged;
}  

在事件处理程序中,我们调用“派生”对象上相同的事件,只是将 this 引用传递给事件的一个参数,而不是其他参数。

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

OneWayEventWraps 属性的第三个参数定义的要替换为 this 引用的参数——在本例中是“sender”,我们用 this 替换参数“sender”——即 PropertyChangedEventHandler 的第一个参数。

这是必要的,因为否则,传递给 PropertyChangedthis 引用将是“基”Person 对象的引用,而不是派生 SelectablePerson 对象的引用,因此 WPF 绑定将不会注意到 SelectablePersonIsItemSelected 属性的变化。

在下一个示例中,我们将展示使用 OneWayEventWraps 属性包装 ItemSelectedChangedEvent 也是更好的选择。

以非常类似的方式,SelectableCar “派生”自 SelectableCar

[OneWayEventWraps(typeof(Selectable), "PropertyChanged", "sender")]
[Wraps(typeof(Selectable), WrappedItemsKind.Event, "ItemSelectedChangedEvent")]
[Wraps(typeof(Selectable), WrappedItemsKind.Property, "IsItemSelected")]
[Wraps(typeof(Selectable), WrappedItemsKind.Method, "ToggleIsSelected")]
[Wraps(typeof(Car), WrappedItemsKind.Property, "Make", "Model")]
public partial class SelectableCar : ISelectable, INotifyPropertyChanged
{
    public SelectableCar(string make, string model)
    {
        this.TheSelectable = new Selectable();

        this.TheCar = new Car { Make = make, Model = model};
    }
}

现在,看看 CollectionWithUniqueItemSelection<T> 类。此类在《基于 View-ViewModel 的 WPF 和 XAML 实现模式》文章中有详细描述。此集合为其项定义了唯一的选中行为,提供了所需的选中和取消选中功能。

CollectionWithUniqueItemSelection<T> 派生自 ObservableCollection<T>,其中 T 应该是 ISelectableCollectionWithUniqueItemSelection<T> 类还定义了 SelectedItem 属性——当没有项被选中时,它为 null;否则,它被设置为当前选中的项。

类中的代码确保 SelectedItem 始终指向 IsItemSelected 属性设置为 true 的唯一项。这是此类完整的代码

public class CollectionWithUniqueItemSelection<T> : ObservableCollection<T>
    where T : class, ISelectable
{
    T _selectedItem = null;
    public T SelectedItem
    {
        get
        {
            return _selectedItem;
        }

        set
        {
            if (_selectedItem == value)
                return;

            if (_selectedItem != null) // unselect old item
            {
                _selectedItem.IsItemSelected = false;
            }

            _selectedItem = value;

            if (_selectedItem != null) // select the new item
            {
                _selectedItem.IsItemSelected = true;
            }

            OnPropertyChanged(new PropertyChangedEventArgs("SelectedItem"));
        }
    }

    void Init()
    {
        ConnectItems(this);
        this.CollectionChanged += CollectionWithUniqueItemSelection_CollectionChanged;
    }

    void CollectionWithUniqueItemSelection_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
    {
        DisconnectItems(e.OldItems);

        ConnectItems(e.NewItems);
    }

    void DisconnectItems(IEnumerable items)
    {
        if (items == null)
            return;

        foreach (T item in items)
        {
            item.ItemSelectedChangedEvent -= item_ItemSelectedChangedEvent;
        }
    }

    void ConnectItems(IEnumerable items)
    {
        if (items == null)
            return;

        foreach (T item in items)
        {
            item.ItemSelectedChangedEvent += item_ItemSelectedChangedEvent;
        }
    }


    void item_ItemSelectedChangedEvent(ISelectable item)
    {
        if (item.IsItemSelected)
        {
            this.SelectedItem = (T)item;
        }
        else
        {
            this.SelectedItem = null;
        }
    }

    public CollectionWithUniqueItemSelection()
    {
        Init();
    }

    public CollectionWithUniqueItemSelection(IEnumerable<T> collection) : base(collection)
    {
        Init();
    }
}  

我们通过派生自 CollectionWithUniqueItemSelection<T> 并分别用 SelectablePersonSelectableCar 对象填充它们来构建我们的 ViewModel。

public class MyTestSelectablePersonCollection : CollectionWithUniqueItemSelection
{
    public MyTestSelectablePersonCollection()
    {
        Add(new SelectablePerson("Joe Doe"));
        Add(new SelectablePerson("Jane Dane"));
        Add(new SelectablePerson("Jack Flack"));
        Add(new SelectablePerson("Judi Moodi"));
    }
}  
public class MyTestSelectableCarCollection : CollectionWithUniqueItemSelection
{
    public MyTestSelectableCarCollection()
    {
        Add(new SelectableCar("Ford", "Mustang"));
        Add(new SelectableCar("Chevy", "Cavalier"));
        Add(new SelectableCar("Toyota", "Corolla"));
        Add(new SelectableCar("Honda", "Civic"));
    }
} 

现在看一下 MainWindow.xaml 文件。我们在其 Resources 部分的顶部定义了我们的 ViewModel。

<local:MyTestSelectablePersonCollection x:Key="TheTestSelectablePersonCollection" />
<local:MyTestSelectableCarCollection x:Key="TheTestSelectableCarCollection" />      

按钮行在文件底部定义为 ItemControl

<ItemsControl x:Name="PersonSelectionControl"
              Style="{StaticResource SelectableItemsControlStyle}"
              ItemsSource="{StaticResource TheTestSelectablePersonCollection}"
              local:AttachedProps.ItemInternalDataTemplateProperty="{StaticResource PersonDataTemplate}" />

<ItemsControl x:Name="CarSelectionControl"
              Style="{StaticResource SelectableItemsControlStyle}"
              ItemsSource="{StaticResource TheTestSelectableCarCollection}"
              local:AttachedProps.ItemInternalDataTemplateProperty="{StaticResource CarDataTemplate}" 
              Grid.Row="1"/>  

SelectableItemControlStyle 样式使用 SelectableItemDataTemplate 资源为单个项定义 ItemTemplate

<DataTemplate x:Key="SelectableItemDataTemplate">
    <Grid Background="Transparent"
          x:Name="ItemPanel">
        <Border x:Name="TheItemBorder"
                Background="Black"
                BorderBrush="White"
                BorderThickness="1">
            <ContentControl Content="{Binding}"
                            ContentTemplate="{Binding Path=(local:AttachedProps.ItemInternalDataTemplate),
                                                      RelativeSource={RelativeSource AncestorType=ItemsControl}}" />
        </Border>
        <Border x:Name="TheOpacityBorder"
                Background="White"
                Opacity="0.5" />
        <i:Interaction.Triggers>
            <!-- Call ToggleSelection method when when MouseDown event 
                 is fired on the item -->
            <i:EventTrigger EventName="MouseDown">
                <ei:CallMethodAction MethodName="ToggleIsSelected"
                                     TargetObject="{Binding Path=DataContext, ElementName=ItemPanel}" />
            </i:EventTrigger>
        </i:Interaction.Triggers>
    </Grid>
    <DataTemplate.Triggers>
        <!-- make TheOpacityBorder less opaque when the mouse is over-->
        <Trigger Property="IsMouseOver"
                 Value="True">
            <Setter TargetName="TheOpacityBorder"
                    Property="Opacity"
                    Value="0.3" />
        </Trigger>
        <DataTrigger Binding="{Binding Path=IsItemSelected}"
                     Value="True">
            <!-- make TheOpacityBorder completely transparent when the 
                 item is selected-->
            <Setter TargetName="TheOpacityBorder"
                    Property="Opacity"
                    Value="0" />

            <!-- make the ItemPanel non-responsive to the event when
                 the item is selected (this is to prevent unselection
                 when clicking on the same item again) -->
            <Setter TargetName="ItemPanel"
                    Property="IsHitTestVisible"
                    Value="False" />
        </DataTrigger>
    </DataTemplate.Triggers>
</DataTemplate>  

正如您所见,每个“按钮”的亮/暗度是通过数据触发器根据 IsItemSelected 属性是 true 还是 false 来控制的。

<DataTrigger Binding="{Binding Path=IsItemSelected}"
             Value="True">
    <!-- make TheOpacityBorder completely transparent when the 
         item is selected-->
    <Setter TargetName="TheOpacityBorder"
            Property="Opacity"
            Value="0" />

    <!-- make the ItemPanel non-responsive to the event when
         the item is selected (this is to prevent unselection
         when clicking on the same item again) -->
    <Setter TargetName="ItemPanel"
            Property="IsHitTestVisible"
            Value="False" />
</DataTrigger>  

此外,使用 Microsoft Expression Blend SDK 触发器,每当项被单击时,我们都会在相应项的 ViewModel 上调用 ToggleIsSelected() 方法。

<i:Interaction.Triggers>
    <!-- Call ToggleSelection method when when MouseDown event 
         is fired on the item -->
    <i:EventTrigger EventName="MouseDown">
        <ei:CallMethodAction MethodName="ToggleIsSelected"
                             TargetObject="{Binding Path=DataContext, ElementName=ItemPanel}" />
    </i:EventTrigger>
</i:Interaction.Triggers>  

此外,每个“按钮”的内容由附加的 AttachedProps.ItemInternalDataTemplate 属性确定。

<Border x:Name="TheItemBorder"
        Background="Black"
        BorderBrush="White"
        BorderThickness="1">
    <ContentControl Content="{Binding}"
                    ContentTemplate="{Binding Path=(local:AttachedProps.ItemInternalDataTemplate),
                                              RelativeSource={RelativeSource AncestorType=ItemsControl}}" />
</Border>  

对于 Person 行,此属性设置为 PersonDataTemplate,对于 Car 行,则设置为 CarDataTemplate

<DataTemplate x:Key="PersonDataTemplate">
    <TextBlock HorizontalAlignment="Center"
               VerticalAlignment="Center"
               Foreground="White"
               Text="{Binding Path=Name}"
               Margin="10, 5" />
</DataTemplate>

<DataTemplate x:Key="CarDataTemplate">
    <StackPanel Orientation="Horizontal"
                Margin="5">
        <TextBlock HorizontalAlignment="Center"
                   VerticalAlignment="Center"
                   Foreground="White"
                   FontWeight="Bold"
                   Text="{Binding Path=Make}"
                   Margin="5, 0" />
         <TextBlock HorizontalAlignment="Center"
                   VerticalAlignment="Center"
                   Foreground="Green"
                   Text="{Binding Path=Model}"
                   Margin="5, 0" />
    </StackPanel>
</DataTemplate>

运行应用程序时,您可以看到 CollectionWithUniqueItemSelection 类正确地强制每行最多只有一个选中的项,尽管 SelectablePersonSelectableCar 类都不是(在严格的 C# 意义上)派生自 Selectable!这意味着多态性在基于包装器的继承中有效!

唯一选择和选中项显示示例

当您运行 SelectedItemDisplaySample 项目并单击某些按钮时,您将看到以下内容

右侧将显示选定人员的姓名。

SelectedItemDisplaySample 的代码与上一个示例非常相似(除了缺少与 Car 相关的类)。

MainWindow.xaml 文件底部有一个小补充——我们添加了显示选定人员姓名的功能。

<Grid Grid.Column="1"
      HorizontalAlignment="Center"
      VerticalAlignment="Center"
      DataContext="{Binding Source={StaticResource TheTestSelectablePersonCollection}}">
    <Grid.RowDefinitions>
        <RowDefinition Height="*" />
        <RowDefinition Height="*" />
    </Grid.RowDefinitions>
    <TextBlock Text="Selected Person:"/>
    <TextBlock Grid.Row="1"
               Text="{Binding SelectedItem.Name}" />
</Grid>  

正如您所见,底部的 TextBlock 绑定到了 CollectionWithUniqueItemSelectionSelectedItem.Name 路径。

现在,如果您查看 SelectablePerson 类,您会发现一个有趣的更改——ItemSelecteChangedEvent 现在使用 OneWayEventWraps 属性定义,而不是像上一个示例那样使用 Wraps 属性。

[OneWayEventWraps(typeof(Selectable), "PropertyChanged", "sender")]
[OneWayEventWraps(typeof(Selectable), "ItemSelectedChangedEvent", "obj")]
//[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 SelectablePerson : ISelectable, INotifyPropertyChanged
{
    public SelectablePerson(string name) 
    {
        this.TheSelectable = new Selectable();
        this.ThePerson = new Person { Name = name };
    }
}  

事实上,如果您尝试取消注释 Wraps 并注释掉 OneWayEventWraps 行,该示例将停止工作。原因与上面关于 PropertyChanged 事件的解释以及《基于 Roslyn 的模拟多重继承用法模式 (第一部分)》文章中的解释非常相似。

如果没有 OneWayEventWraps 属性,ItemSelectedChangedEvent 会将 SelectedItem 设置为 Selectable“基”对象,而不是完整的 SelectablePerson 对象。Selectable“基”对象没有 Name 属性,因此不会显示任何内容。然而,OneWayEventWraps 属性会强制将 SelectedItem 属性设置为正确的 SelectablePerson 对象。

可选项和可描述项示例

在此示例中,我们将展示我们可以“继承”自三个类而不是两个类,而且这三个“部分”组合在一起也能正常工作。

要开始示例,请打开并运行 SelectableAndDescribableItemSample 项目。您将看到以下内容(单击一些按钮后)

现在,每个 Person 项在右侧的 TextBox 中都有一个描述。此外,您可以更改描述,单击其他按钮,然后再次单击同一人,您将看到描述的更改已被记录。

此示例的代码与上一个示例非常相似。它有一个额外的类 Descrabable,其中包含一个 Description 属性,该属性在更改时触发 PropertyChanged 事件。

public class Describable : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    void OnPropertyChanged(string propName)
    {
        if (PropertyChanged != null)
            PropertyChanged(this, new PropertyChangedEventArgs(propName));
    }


    #region Description Property
    private string _description;
    public string Description
    {
        get
        {
            return this._description;
        }
        set
        {
            if (this._description == value)
            {
                return;
            }

            this._description = value;
            this.OnPropertyChanged("Description");
        }
    }
    #endregion Description Property

} 

SelectableDesribablePerson 类对 SelectableItemSelectedChangedEvent 以及 SelectableDescribable “超类”的 PropertyChanged 事件使用 OneWayEventWraps 属性。

[OneWayEventWraps(typeof(Selectable), "ItemSelectedChangedEvent", "obj")]
[OneWayEventWraps(typeof(Selectable), "PropertyChanged", "sender")]
[OneWayEventWraps(typeof(Describable), "PropertyChanged", "sender")]
[Wraps(typeof(Selectable), WrappedItemsKind.Property, "IsItemSelected")]
[Wraps(typeof(Selectable), WrappedItemsKind.Method, "ToggleIsSelected")]
[Wraps(typeof(Describable), WrappedItemsKind.Property, "Description")]
[Wraps(typeof(Person), WrappedItemsKind.Property, "Name")]
public partial class SelectableDescribablePerson : ISelectable, INotifyPropertyChanged
{
    public SelectableDescribablePerson(string name, string description = null)
    {
        this.TheSelectable = new Selectable();
        this.TheDescribable = new Describable { Description = description };

        this.ThePerson = new Person { Name = name };
    }
}  

实现菱形继承

菱形继承发生在两个类继承自一个类,然后第四个类继承自这两个类时。

我们可能希望 SelectableBatman 类继承自 SelectableBatSelectableMan,而不是从 SectableBatMan 继承,这有多种原因。

主要原因是,我们可能希望在 SelectableBat 中保留 SelectableBat 之间的某些连接,在 SelectableMan 中保留 SelectableMan 之间的连接。

其他原因可能涉及封装,例如,如果 Selectable 可能是一个内部类,并且我们无法访问它进行派生,或者在我们的宇宙中 BatMan 对象总是可选项,而不需要不可选项的 BatMan 类。

在 C++ 中,菱形继承是通过关键字 virtual 实现的。该关键字必须在 SelectableBatSelectableMan 中的一个继承自 Selectable 的早期阶段出现。这很不方便,而且完全不必要——您必须选择哪个类将依赖于另一个类的实现,然后您才能真正需要创建 SelectableBatman 类。此外,如果您虚拟继承 SelectableBat 而非虚拟继承 SelectableMan,那么将来是否需要创建其他组合,其中 SelectableBat 将提供自己的 Selectable 实现,而 SelectableMan 反之亦然?

我们将展示如何使用我们的包装器继承来解决这个问题。

菱形继承示例位于 DiamondInheritanceSample 项目下。

这是您在运行项目并单击一些按钮时看到的内容

现在,让我们看一下代码。

Selectable 类与先前项目中的类完全相同。

Bat 类只有一个简单的属性 IsFlying,表示 Bat 对象当前是否正在飞行。

public class Bat
{
    public bool IsFlying { get; set; }
}  

有人可能会注意到,IsFlying 属性实际上应该在更改时触发 PropertyChanged 事件以显示某些视觉变化,但我们试图让示例尽可能轻量级——因此对我们的目的来说,这并不必要。

Man 类有两个简单的属性:NameIsWalking——前者指定人的姓名,后者指定人当前是否正在行走。

public class Man
{
    public string Name { get; set; }

    public bool IsWalking { get; set; }
}  

SelectableBat 派生自 SelectableBat

[OneWayEventWraps(typeof(Selectable), "PropertyChanged", "sender")]
[Wraps(typeof(Selectable), WrappedItemsKind.Event, "ItemSelectedChangedEvent")]
[Wraps(typeof(Selectable), WrappedItemsKind.Property, "IsItemSelected")]
[Wraps(typeof(Selectable), WrappedItemsKind.Method, "ToggleIsSelected")]
[Wraps(typeof(Bat), WrappedItemsKind.Property, "IsFlying")]
public partial class SelectableBat
{
}  

SelectableMan 派生自 SelectableMan

[OneWayEventWraps(typeof(Selectable), "PropertyChanged", "sender")]
[Wraps(typeof(Selectable), WrappedItemsKind.Event, "ItemSelectedChangedEvent")]
[Wraps(typeof(Selectable), WrappedItemsKind.Property, "IsItemSelected")]
[Wraps(typeof(Selectable), WrappedItemsKind.Method, "ToggleIsSelected")]
[Wraps(typeof(Man), WrappedItemsKind.Property, "IsWalking", "Name")]
public partial class SelectableMan
{
}  

SelectableBatman 派生自 SelectableBatSelectableMan,并且 Selectable 部分的共享在 SelectableBatman 的构造函数中得到保证。

[OneWayEventWraps(typeof(SelectableMan), "PropertyChanged", "sender")]
[Wraps(typeof(SelectableMan), WrappedItemsKind.Event, "ItemSelectedChangedEvent")]
[Wraps(typeof(SelectableMan), WrappedItemsKind.Property, "IsItemSelected")]
[Wraps(typeof(SelectableMan), WrappedItemsKind.Method, "ToggleIsSelected")]
[Wraps(typeof(SelectableMan), WrappedItemsKind.Property, "IsWalking", "Name")]
[Wraps(typeof(SelectableBat), WrappedItemsKind.Property, "IsFlying")]
public partial class SelectableBatMan : ISelectable, INotifyPropertyChanged
{
    public SelectableBatMan(string name, bool isWalking, bool isFlying)
    {
        // shared Selectable object
        Selectable selectable = new Selectable();

        // pass the same Selectable to SelectableMan
        // 'super-class'
        this.TheSelectableMan = new SelectableMan
        {
            TheMan = new Man
            {
                Name = name,
                IsWalking = isWalking,
            },
            TheSelectable = selectable
        };

        // and pass the same Selectable to 
        // SelectableBat 'super-class'
        this.TheSelectableBat = new SelectableBat
        {
            TheBat = new Bat { IsFlying = isFlying},
            TheSelectable = selectable
        };
    }
}  

正如您所看到的——我们在构造函数中创建一个单一的 Selectable 对象,并将其传递给 SelectableManSelectableBat 的“基类”构造函数。

这就是确保 SelectableBatmanSelectableBatSelectableMan 部分共享 Selectable 部分所需的一切!

结论

我们已经展示了如何在 C# 中使用自动生成的包装器实现多重继承。我们还表明,通过使用接口可以保留多态性或基于包装器的子类。

最后,我们提供了一个示例,展示了使用生成的包装器实现著名的菱形继承是多么简单。事实上,我们的解决方案比 C++ 内置的解决方案要好得多——它在创建超类时不会施加任何僵化的限制。

© . All rights reserved.