为类型 T 编写扩展方法;如何为 T 的一个字段添加类型约束?

3

初始情况:

我正在使用一种专有的框架(ESRIArcGIS Engine)进行操作,我想要为其添加一些新的功能。我选择使用C#中的扩展方法来完成。

以下是与此问题相关的框架API的部分:

    +------------------------+                   IGeometry
    |  IFeature <interface>  |                   <interface>
    +------------------------+                       ^
    |  +Shape: IGeometry     |                       |
    +------------------------+             +---------+---------+
                                           |                   |
                                        IPoint              IPolygon
                                        <interface>         <interface>

我想要做的事情:

我想为IFeature编写一个扩展方法,使其能够实现以下功能:

IFeature featureWithPointShape   = ...,
         featureWithPolygonShape = ...;

// this should work:
featureWithPointShape.DoSomethingWithPointFeature();

// this would ideally raise a compile-time error:
featureWithPolygonShape.DoSomethingWithPointFeature();

问题在于点和多边形形状(IPointIPolygon)都被包装在同一类型中(IFeature),扩展方法是定义在该类型上的。扩展方法必须在 IFeature 上,因为我只能从 IFeature 得到其 IGeometry,但反过来不行。

问题:

虽然可以在运行时轻松检查 IFeature 对象的 Shape 类型(请参见下面的代码示例),但我如何在编译时实现此类型检查呢?

public static void DoSomethingWithPointFeature(this IFeature feature)
{
    if (!(feature.Shape is IPoint))
    {
        throw new NotSupportedException("Method accepts only point features!");
    }
    ...  // (do something useful here)
}

有没有可能使用通用的包装类型FeatureWithShape<IPoint>作为IFeature的包装器,并在该包装类型上定义扩展方法,然后将所有IFeature对象转换成此包装器类型呢?

你可以创建一个 FeatureWithShape<IPoint> : IFeature 的包装器,并指定一个显式的操作符:http://msdn.microsoft.com/en-us/library/xhbhezf4(VS.90).aspx - Joseph Yaduvanshi
@Jim Schubert: 听起来很有趣,你能提供更多关于如何实现这个的细节吗?我不想重新实现整个 IFeature 接口(它包含了无数东西)。有没有一种方法可以实现一个接口类型的包装器,它的行为与接口类型本身完全相同,但将大部分实际工作委托给被包装的对象(该对象是接口类型)? - stakx - no longer contributing
这是一个有点不寻常的用例,但你可能可以通过模拟来实现。您可以设置模拟以将调用委托给底层接口(您包装的对象),然后添加另一个自定义接口实现,以公开强类型形状。基本上,它在运行时发出一个执行委派的类。然而,对于您正在进行的操作,这可能是一种有点笨重的解决方案。有关模拟的示例,请参见Moq:http://code.google.com/p/moq/wiki/QuickStart - Dan Bryant
5个回答

1

将您的 IFeature 接口也泛型化:

IFeature<IPoint>
IFeature<IPolygon>

然后,您可以对IFeature的内部类型设置约束。


我无法更改IFeature,因为它是专有框架的一部分。 - stakx - no longer contributing
然后你可以这样做:MyFeature<T> : IFeature - 这样你就可以使用通用接口,并且拥有完整的编译器检查,同时你的通用类型也是一个可以传递给框架的IFeature。 - Sebastian P.R. Gingter
感谢您迄今为止的输入。不幸的是,这也行不通,因为IFeature对象是由框架创建的。由于框架不知道MyFeature<T>,我永远不会得到该类型的实例。而且我不知道有什么方法可以将对象提升到派生类型而不在其周围放置包装器。(您有任何办法吗?) - stakx - no longer contributing

1

根据定义,如果您有一个 IFeature 对象,则其 Shape 属性可以包含实现了 IGeometry 接口的任何类型的值。如果您控制 IFeature 对象的实例化,则可以创建自己的泛型类来实现 IFeature,或从实现 IFeature 的框架类派生一个类,然后可以轻松约束 Shape 的类型。如果您无法控制这些对象的实例化,则可能会受到运行时检查的限制。

如果您正在使用 .NET 4.0,则可以使用代码合同。静态检查器将在您的扩展方法对 Shape 类型有前置条件时给出编译时警告。


+1 建议使用代码契约(Code Contracts)。(顺便说一下,我以前用过它,是在 VS 2008 中,但我没想到在这个特定的情况下使用它。)你写的正是我担心的。由于我无法控制 IFeature 的实例化,因此可能没有比使用代码契约更好的解决方案来解决这个问题了。 - stakx - no longer contributing
哦,我刚刚注意到Code Contracts也行不通。如果我想要在编译时警告可能的运行时类型不匹配,我必须以某种方式在代码中表达预期的类型。仅有“IFeature”将永远无法实现这一点。感谢您引导我的思路! - stakx - no longer contributing

1

我认为你无法使用ArcObjects中的IFeature接口在编译时实现此检查。

几何类型取决于加载要素的要素类的定义。直到运行时,你才能知道这一点。


1

我认为添加一个仅适用于点要素的扩展方法到IFeature接口中是一个糟糕的设计。接口IFeature被所有类型的几何图形(点、线和面)实现。这意味着当扩展IFeature接口时,扩展方法也应该被设计成支持所有类型的几何图形。显然情况并非如此 :-)

当你必须扩展IFeature时,请像你写的那样在运行时检查形状类型。在我看来,这是你问题的最佳解决方案。


我理解你的观点,认为它是有道理的。可能框架存在设计缺陷;另一方面,如果需要的话,也许我应该直接在IPointIPolygon等上定义操作。 (为了正确起见:几何图形不实现IFeature;它们可以被包含在一个IFeature对象中。) - stakx - no longer contributing

0

有趣的问题,尤其是对我来说,因为我(必须)以ArcObjects编程为生。

编辑,警告:这种方法行不通。它会在运行时失败。我会把它留在这里以示惭愧。

采用Sebastian P.R. Gingter的接口继承建议并加以运用,我定义了一个接口IFeatureOf<T>,该接口继承了IFeature。这个新接口唯一的好处就是在声明要素时添加更多信息。

但是,如果您事先知道您正在处理点要素,则可以将这些要素声明为IFeatureOf<IPoint>并将其提供给期望包含点几何体的要素的函数。

当然,您仍然可以将来自面要素类的要素声明为var notReallyAPointFeature = (IFeatureOf<IPoint>)myPolygonFeature;,但是如果您事先知道要素类型并使用IFeatureOF<>来限制它,那么如果您将其提供给专门的函数,您将在编译时出现错误。

以下是一个小例子:

using ESRI.ArcGIS.Geometry;
using ESRI.ArcGIS.Geodatabase;

class Program
{
    public interface IFeatureOf<T> : IFeature { };

    public static void AcceptsAllFeatures(IFeature feature) {
        //do something, not caring about geometry type...
        return;
    }

    public static void AcceptsOnlyPointFeatures(IFeatureOf<IPoint> pointFeature) {
        IPoint pointGeometry = (IPoint)pointFeature.Shape; //constraint guarantees this is OK
        //do something with pointGeometry
        return;
    }

    static void Main(string[] args)
    {
        IFeature pointFeature = new FeatureClass(); //this is where you would read in a feature from your data set
        IFeature polylineFeature = new FeatureClass();
        var constainedPointFeature = (IFeatureOf<IPoint>)pointFeature;
        var constrainedPolylineFeature = (IFeatureOf<IPolyline>)polylineFeature;

        AcceptsAllFeatures(constainedPointFeature);             //OK
        AcceptsAllFeatures(constrainedPolylineFeature);         //OK
        AcceptsAllFeatures(pointFeature);                       //OK
        AcceptsAllFeatures(polylineFeature);                    //OK

        AcceptsOnlyPointFeatures(constainedPointFeature);       //OK

        AcceptsOnlyPointFeatures(constrainedPolylineFeature);   //Compile-time error: IFeatureOf<IPolyline> != IFeatureOf<IPoint>
        AcceptsOnlyPointFeatures(pointFeature);                 //Compile-time error: IFeature != IFeatureOf<something>
        AcceptsOnlyPointFeatures(polylineFeature);              //Compile-time error: IFeature != IFeatureOf<something>
    }
}

虽然这似乎是一个很好的解决方案,但由于这个关键步骤:var constainedPointFeature = (IFeatureOf<IPoint>)pointFeature; -- pointFeature后面的协同类没有实现IFeatureOf<IPoint>,因此会引发InvalidCastException。至少在我的机器上是这样的...? - stakx - no longer contributing
@stakx:你说得对。我需要更多关于接口和继承方面的阅读。我希望有更多类型安全的ArcObjects,但显然这不是一件容易的事情。 - cfern
1
我认为自定义功能可能是实现这一目标的一种方式。 http://edndoc.esri.com/arcobjects/9.0/ExtendingArcObjects/Ch07/TreeCustomFeature.htm 我非常有兴趣看到用C#而不是C++实现这个的方法。我想,如果有一种使用C#进行COM聚合的方法,那么这应该是可行的(COM_INTERFACE_ENTRY_AGGREGATE_BLIND)。如果可能的话,我认为这将成为一个很好的维基条目。 - Kirk Kuykendall

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接