【问题标题】:Strange problem with .Net inheritance and member visibility.Net 继承和成员可见性的奇怪问题
【发布时间】:2010-07-15 14:02:48
【问题描述】:

我在 VB.Net 类库中有一个问题,我已将其大大简化为以下内容...

Public MustInherit Class TargetBase

End Class

Public Class TargetOne
    Inherits TargetBase
End Class

Public Class TargetTwo
    Inherits TargetBase
End Class

Public Class TargetManager
    Public Sub UpdateTargets(ByVal Targets As List(Of TargetBase))
        For Each objTarget As TargetBase In Targets
            UpdateTarget(objTarget)
        Next
    End Sub

    Private Sub UpdateTarget(ByVal Value As TargetOne)

    End Sub

    Private Sub UpdateTarget(ByVal Value As TargetTwo)

    End Sub
End Class

由于UpdateTarget(objTarget) 行上的语法错误,这将无法编译 - 重载解析失败,因为在没有缩小转换的情况下无法调用可访问的“UpdateTarget”

所以我将 For-Each 循环更改为使用 Object 而不是 TargetBase...

For Each objTarget As Object In Targets
    UpdateTarget(objTarget)
Next

现在可以编译,但出现运行时错误 - 未找到类型“TargetManager”的公共成员“UpdateTarget”。

所以我采取了明显的下一步,将 2 个 UpdateTarget() 重载设为公共(而不是私有)。

Public Sub UpdateTarget(ByVal Value As TargetOne)

End Sub

Public Sub UpdateTarget(ByVal Value As TargetTwo)

End Sub

现在可以了!

我几乎可以理解为什么将其更改为 Object 会起作用,但是当我仅在同一个类中调用它们时为什么必须将这些方法设为 Public - 我宁愿它们在此类之外不可用。

谁能解释一下?

提前致谢(抱歉这个问题太长了!)

附加 到目前为止,感谢大家的回答。我已经找到了使它工作的解决方法(使 UpdateTarget 方法公开)。另一种解决方法是在调用 UpdateTarget 之前对 objTarget 和 DirectCast 进行 TypeOf 检查,例如...

For Each objTarget As Object In Targets
    If TypeOf objTarget Is TargetOne Then
        UpdateTarget(DirectCast(objTarget, TargetOne))
    ElseIf TypeOf objTarget Is TargetTwo Then
        UpdateTarget(DirectCast(objTarget, TargetTwo))
    End If
Next

这也可行 - 我发布了这个问题,因为我真的很想了解为什么将 UpdateTarget 的可见性从 Private 更改为 Public 会消除运行时错误,这完全违背了我的理解!

【问题讨论】:

  • 您的意思是像这样构造您的 UpdateTarget 方法吗?您不是说在每个 TargetBase 子类中都有一个 UpdateTarget 方法吗?然后你可以调用objTarget.UpdateTarget。您之前可能遇到过这个编译错误,因为 .net 不知道它正在传递哪个类(缩小转换)
  • 只是出于好奇“这现在有效”的意思——它调用了 TargetBase 类型的相关方法(即正确);还是它总是调用其中一种方法?
  • @Jugglingnutcase - 感谢您的回答。我已经考虑过这一点,不幸的是这是不可能的。我展示的代码示例非常简化(只是为了说明问题) - 在我的实际项目中,我无法访问 TargetBase,因此无法向其添加 UpdateTarget 方法。
  • @Adam:它的工作原理是调用了正确的 UpdateTarget 方法
  • 出于更多好奇 - 如果您从私有更改为公共但保留 TargetBase 的原始传递,而不是强制转换为对象 - 它是否有效?

标签: .net inheritance oop


【解决方案1】:

在我看来它无法决定使用什么方法,因为您使用的是两个方法参数的基本类型。 TargetOne/Two 都是有效的 TargetBase,因此这两种方法在解析引擎看来是一样的——这意味着它无法选择。

但是,我不知道为什么其他更改使它起作用...让我想想,更新待定。

在 C# 中,我没有遇到此问题,因为您无法将强制转换 TargetBase 转发到 TargetOne 或 TargetTwo... 它会产生不同的编译器错误 - 方法的参数无效,因为它无法将基数隐式转换为派生数。您提到的第一个编译器错误基本上是 VB.NET 的等价物。

我发现了这个链接,但我不确定它是用于 VB 还是 VB.NET - 无论哪种方式,有趣的阅读: http://msdn.microsoft.com/en-us/library/tb18a48w.aspx

这也可能与Option Strict 和 VB.NET 2010 中的协方差有关。这篇文章有一点重载部分,可能会很有用: http://msdn.microsoft.com/en-us/magazine/ee336029.aspx

更新:请注意,我不知道它为什么突然起作用,这听起来像是 Jon Skeet 或 Eric Lippert 的一个。

更新 2: 我可以建议的一件事是针对每种情况(私有到公共/使用对象)编译应用程序并使用反射器查看 IL。基本上,寻找任何差异 - 可能是编译器在后台为您添加了一些东西 - 或者运行时能够根据当前类型确定正确的方法。

更新 3:我想我明白了。此引用来自以下链接:

"一个对象被提前绑定 分配给声明为的变量 特定对象类型。”

http://visualbasic.about.com/od/usingvbnet/a/earlybind.htm

表示当您指定 TargetBase 时,它​​是早期绑定并且编译器会抱怨。当您指定对象时,它是后期绑定并且运行时在其私有重新此链接时抱怨:

http://msdn.microsoft.com/en-us/library/h3xt2was(VS.80).aspx

因此为您指定公共作品。运行时显然能够后期绑定到正确的重载 - VB.NET 为您隐藏的后期绑定的一个很好的功能:-)

【讨论】:

  • 谢谢亚当。您的重载解决方案链接非常有用。我猜我的代码在第 1 点失败)可访问性 - 但我不明白为什么
  • @barrylloyd 我相信它与编译器一起失败了——但它是如何成功的却是个谜。编译器/运行时必须为你做点什么——也许审查 IL 会产生一些结果。
  • 谢谢亚当。从您的更新 3 我到msdn.microsoft.com/en-us/library/0tcf61s1(v=VS.80).aspx,它有一个黄色的注释说...'后期绑定只能用于访问声明为公共的类型成员。访问声明为 Friend 或 Protected Friend 的成员会导致运行时错误。
【解决方案2】:

虽然VB不是我的专长,但我想我可以正确回答你的问题。

在您的程序的第一个版本中,您有UpdateTarget(objTarget),其中 objTarget 是 TargetBase 类型。 VB原因如下:

  • 呼叫的接收方是“我”。它有一个众所周知的编译时类型 TargetManager。
  • 调用的参数是“objTarget”。它有一个众所周知的编译时类型 TargetBase。
  • 由于接收者和参数都有类型,我们应该做重载决议来决定调用哪个版本的 UpdateTarget。
  • 重载解决方案确定两个版本的 UpdateTarget 都需要从 TargetBase 到更具体的类型的潜在不安全转换。
  • 因此重载解析失败。

在第二个版本中,objTarget 是 Object 类型。 VB原因如下。

  • 调用的参数是对象类型。
  • 同样,重载决议在这里没有给我们带来任何好处。
  • 由于重载解析失败且对象类型为 Object 类型,并且 Option Strict 未启用,请生成代码以再次执行分析在运行时使用 runtime 类型论据。
  • 后期绑定分析要求调用的方法是public。为什么?因为假设这段代码不是在 TargetManager 中。您是否希望能够通过后期绑定而不是通过早期绑定从 TargetManager 外部调用私有方法?可能不是。这似乎是危险和错误的。不幸的是,VB 的后期绑定不区分inside TargetManager 完成的后期绑定和outside 完成的后期绑定。它只是requires that the methods be public,以便被称为后期绑定。

在第三个版本中,我们进行后期绑定,后期绑定在运行时成功。

我会在这里做的是:

  1. 开启选项严格。

  2. 做两个 循环。是的,这不是那么有效 因为你做了两次循环,但是, 大不了。如果这不是最慢的 你程序中的东西那么谁在乎 如果它得到几毫秒 慢一点。

我不确切知道 VB 语法是什么,但在 C# 中我会这样写:

public void UpdateTargets(IEnumerable<TargetBase> targets) 
{
    foreach(var targetOne in targets.OfType<TargetOne>())
        UpdateTarget(targetOne);
    foreach(var targetTwo in targets.OfType<TargetTwo>())
        UpdateTarget(targetTwo);
}

漂亮而简单。将集合检查两次,首先取出 TargetOnes,然后取出 TargetTwos。

(另请注意,如果我没有使用 List 的任何功能,那么我会改为使用 IEnumerable 参数,这样该方法会变得更通用。)

【讨论】:

  • +1 哈哈,第一次做对了——我花了一些时间在谷歌上搜索才得出这个结论,即使这样我也不能说得那么雄辩! VB.NET 不是我的强项——我只知道如何做基础知识。我今天学到的是隐式后期绑定——正如我再次了解到的那样,这是解决方案的根源。
【解决方案3】:

正如亚当所说,编译器不知道它应该调用哪个方法。但是,这看起来像 UpdateTarget 方法应该是每个 Target 类型覆盖的实例方法。这样您就可以遍历列表并在objTarget 上调用UpdateTarget

这样做的另一个好处是您可以更好地封装代码。 TargetManager 不需要知道更新实际做了什么,只需调用它即可。此外,当您编写 TargetThree 时,您无需更改 TargetManager 即可更新新类型。

【讨论】:

  • 感谢您的回答。我已经考虑过这一点,不幸的是这是不可能的。我展示的代码示例非常简化(只是为了说明问题) - 在我的实际项目中,我无法访问 TargetBase,因此无法向其添加 UpdateTarget 方法。
【解决方案4】:

更新:由于您在评论中指出在您的情况下无法使用正常的多态方法来解决此问题,因此我至少强烈建议您将 TargetManager 类更改为接受 TargetBase 参数的单个 UpdateTarget 方法。然后检查该方法中的适当类型。这可以防止潜在的问题...

If TypeOf x Is A Then
    DoSomething(DirectCast(x, A))
ElseIf TypeOf x Is B Then
    DoSomething(DirectCast(x, B))
End If

...到处都是。

换句话说,把那张丑陋的支票放在一个地方:

Public Class TargetManager
    Public Sub UpdateTarget(ByVal target As TargetBase)
        Dim t1 = TryCast(target, TargetOne)
        If t1 IsNot Nothing Then
            UpdateTargetOne(t1)
            Return
        End If

        Dim t2 = TryCast(target, TargetTwo)
        If t2 IsNot Nothing Then
            UpdateTargetTwo(t2)
            Return
        End If
    End Sub

    ' I would also recommend changing the targets parameter type here '
    ' to IEnumerable(Of TargetBase), as that is all you need to do '
    ' a For Each loop. '
    Public Sub UpdateTargets(ByVal targets As IEnumerable(Of TargetBase))
        For Each objTarget As TargetBase In Targets
            UpdateTarget(objTarget)
        Next
    End Sub

    Private Sub UpdateTargetOne(ByVal target As TargetOne)
        ' Do something. '
    End Sub

    Private Sub UpdateTargetTwo(ByVal target As TargetTwo)
        ' Do something. '
    End Sub
End Class

你有 polymorphism 倒退。

马上,这是我直觉上认为你真的希望它工作的方式:

Public MustInherit Class TargetBase
    Protected Friend MustOverride Sub Update()
End Class

Public Class TargetOne
    Inherits TargetBase

    Protected Friend Overrides Sub Update()
    End Sub
End Class

Public Class TargetTwo
    Inherits TargetBase

    Protected Friend Overrides Sub Update()
    End Sub
End Class

Public Class TargetManager
    Public Sub UpdateTargets(ByVal Targets As List(Of TargetBase))
        For Each objTarget As TargetBase In Targets
            objTarget.Update()
        Next
    End Sub
End Class

【讨论】:

  • OP 理解通常的做法,我认为问题更多是关于它开始自动工作的奇怪情况。
  • 感谢 Dan,所有关于重构代码的好建议,以及好的替代解决方法。
猜你喜欢
  • 2011-03-03
  • 2012-09-26
  • 1970-01-01
  • 2010-12-25
  • 2011-03-13
  • 1970-01-01
  • 1970-01-01
  • 2012-10-14
  • 1970-01-01
相关资源
最近更新 更多