在C#中表达类型关系并避免使用长类型参数列表

我有这种情况(急剧简化):

interface IPoint<TPoint> 
   where TPoint:IPoint<TPoint>
{
   //example method
   TPoint Translate(TPoint offset);
}

interface IGrid<TPoint, TDualPoint> 
   where TPoint:IPoint<T
   where TDualPoint:Ipoint
{
   TDualPoint GetDualPoint(TPoint point, /* Parameter specifying direction */);
}

这是典型的实现:

class HexPoint : IPoint<HexPoint> { ... }
class TriPoint : IPoint<TriPoint> { ... }

class HexGrid : IGrid<HexPoint, TriPoint> { ... }
class TriGrid : IGrid<TriPoint, HexPoint> { ... }

因此,在HexGrid ,客户可以拨打电话在双重网格上获得一个点,并使用正确的类型:

TriPoint dual = hexGrid.GetDualPoint(hexPoint, North);

到现在为止还挺好; 客户端并不需要了解两点是如何关联的类型什么的,她需要知道的是,在HexGrid方法GetDualPoint返回一个TriPoint

除...

我有一个充满通用算法的类, IGrid进行操作,例如:

static List<TPoint> CalcShortestPath<TPoint, TDualPoint>(
   IGrid<TPoint, TDualPoint> grid, 
   TPoint start, 
   TPoint goal) 
{...}

现在,客户端突然不得不知道HexPoint的双重点是HexPointTriPoint ,我们需要将它指定为类型参数列表的一部分,即使它对此算法不严格影响:

static List<TPoint> CalcShortestPath<TPoint, *>(
   IGrid<TPoint, *> grid, 
   TPoint start, 
   TPoint goal) 
{...}

理想情况下,我想使DualPoint成为IPoint类型的“属性”,以便HexPoint.DualPointTriPoint类型。

允许IGrid看起来像这样的东西:

interface IGrid<TPoint> 
   where TPoint:IPoint<TPoint> 
   //and TPoint has "property" DualPoint where DualPoint implements IPoint...
{
   IGrid<TPoint.DualPoint> GetDualGrid();
}

和这样的函数CalcShortestPath

static List<TPoint> CalcShortestPath<TPoint>(
   IGrid<TPoint> grid, 
   TPoint start, 
   TPoint goal) 
{...}

据我所知,当然这是不可能的。

但是有没有办法改变我的设计来模仿它? 以便

  • 它表达了这两种类型之间的关系
  • 它防止过多的类型参数列表
  • 它可以防止客户必须仔细考虑具体类型如何“专门化”类型实现的接口的类型参数。

  • 给出一个为什么这成为一个真正的问题的迹象:在我的图书馆IGrid实际上有4个类型参数, IPoint有3个,并且可能会增加(最多6个和5个)。 (大多数这些类型参数之间的关系类似。)

    对于算法显式重载而不是泛​​型是不实际的:每个IGridIPoint有9个具体的实现。 一些算法在两种类型的网格上运行,因此具有一吨类型参数。 (许多函数的声明比函数体更长!)

    当我的IDE在自动重命名期间丢弃所有类型参数时,精神负担被驱回到家中,我必须手动将所有参数恢复。 这不是一个盲目的任务; 我的大脑被炸了。


    正如@Iridium所要求的,一个显示类型推断失败的例子。 显然,下面的代码不会做任何事情; 它只是为了说明编译器的行为。

    using System;
    using System.Collections.Generic;
    using System.Linq;
    
    public interface IPoint<TPoint, TDualPoint> 
       where TPoint:IPoint<TPoint, TDualPoint> 
       where TDualPoint : IPoint<TDualPoint, TPoint>{}
    
    interface IGrid<TPoint, TDualPoint> 
       where TPoint:IPoint<TPoint, TDualPoint>
       where TDualPoint:IPoint<TDualPoint, TPoint>{}
    
    class HexPoint : IPoint<HexPoint, TriPoint> 
    {
       public HexPoint Rotate240(){ return new HexPoint();} //Normally you would rotate the point
    }
    
    class TriPoint : IPoint<TriPoint, HexPoint>{}    
    class HexGrid : IGrid<HexPoint, TriPoint>{}
    
    static class Algorithms
    {  
       public static IEnumerable<TPoint> TransformShape<TPoint, TDualPoint>(
          IEnumerable<TPoint> shape, 
          Func<TPoint, TPoint> transform)
    
       where TPoint : IPoint<TPoint, TDualPoint> 
       where TDualPoint : IPoint<TDualPoint, TPoint> 
       {
          return 
             from TPoint point in shape
                select transform(point);
       }
    
       public static IEnumerable<TPoint> TransformShape<TPoint, TDualPoint>(
          IGrid<TPoint, TDualPoint> grid, 
          IEnumerable<TPoint> shape, 
          Func<TPoint, TPoint> transform)
    
       where TPoint : IPoint<TPoint, TDualPoint> 
       where TDualPoint : IPoint<TDualPoint, TPoint> 
       {
          return 
             from TPoint point in shape
                //where transform(point) is in grid
                select transform(point);
       }
    }
    
    class UserCode
    {  
       public static void UserMethod()
       {
          HexGrid hexGrid = new HexGrid();      
          List<HexPoint> hexPointShape = new List<HexPoint>(); //Add some items
    
          //Compiles
          var rotatedShape1 = Algorithms.TransformShape(
             hexGrid,
             hexPointShape, 
             point => point.Rotate240()).ToList();
    
          //Compiles   
          var rotatedShape2 = Algorithms.TransformShape<HexPoint, TriPoint>(
             hexPointShape, 
             point => point.Rotate240()).ToList(); 
    
          //Does not compile   
          var rotatedShape3 = Algorithms.TransformShape(
              hexPointShape, 
              point => point.Rotate240()).ToList();
       }
    }
    

    所以,要根据我在评论中提到的一次性想法提出答案......

    基本要点是“定义一种传达这种点对偶概念的类型,并将其用于相关签名中,以便为编译器提供所需的提示”

    有一件事你应该阅读,只要你点击可怕的“类型不能推断使用”错误:http://blogs.msdn.com/b/ericlippert/archive/2009/12/10/constraints-are-not-part-的最signature.aspx

    在此,梅斯先生。 Lippert阐明了严酷的事实,即在这个推论阶段只有签名的参数被检查,而不是约束。 所以我们必须在这里做一个更具体的“特定”。

    首先,让我们来定义我们的“二元关系” - 我应该注意到这是建立这些关系的一种方式 ,理论上存在着它们的无限多样性。

    public interface IDual<TPoint, TDualPoint> 
        where TPoint: IPoint<TPoint>, IDual<TPoint, TDualPoint>
        where TDualPoint: IPoint<TDualPoint>, IDual<TDualPoint, TPoint>
    {}
    

    现在我们回过头来改进我们现有的签名:

    public interface IPoint<TPoint> 
       where TPoint:IPoint<TPoint> 
    {}
    class TriPoint : IPoint<TriPoint>, IDual<TriPoint,HexPoint>
    {}
    class HexPoint : IPoint<HexPoint>, IDual<HexPoint,TriPoint> 
    {
       // Normally you would rotate the point
       public HexPoint Rotate240(){ return new HexPoint();} 
    }
    

    同样在“次要类型”上,网格:

    interface IGrid<TPoint, TDualPoint> 
       where TPoint: IPoint<TPoint>, IDual<TPoint, TDualPoint>  
       where TDualPoint : IPoint<TDualPoint>, IDual<TDualPoint, TPoint> 
    {
        TDualPoint GetDualPoint(TPoint point);
    }
    class HexGrid : IGrid<HexPoint, TriPoint>
    {
        public TriPoint GetDualPoint(HexPoint point)
        {
            return new TriPoint();
        }
    }
    class TriGrid : IGrid<TriPoint, HexPoint> 
    {
        public HexPoint GetDualPoint(TriPoint point)
        {
            return new HexPoint();
        }
    }
    

    最后在我们的实用方法上:

    static class Algorithms
    {  
       public static IEnumerable<TPoint> TransformShape<TPoint, TDualPoint>(
          IEnumerable<IDual<TPoint, TDualPoint>> shape, 
          Func<TPoint, TPoint> transform)
       where TPoint : IPoint<TPoint>, IDual<TPoint, TDualPoint>   
       where TDualPoint : IPoint<TDualPoint>, IDual<TDualPoint, TPoint> 
       {
          return 
             from TPoint point in shape
                select transform(point);
       }
    
       public static IEnumerable<TPoint> TransformShape<TPoint, TDualPoint>(
          IGrid<TPoint, TDualPoint> grid, 
          IEnumerable<IDual<TPoint, TDualPoint>> shape, 
          Func<TPoint, TPoint> transform)
       where TPoint : IPoint<TPoint>, IDual<TPoint, TDualPoint>   
       where TDualPoint : IPoint<TDualPoint>, IDual<TDualPoint, TPoint> 
       {
          return 
             from TPoint point in shape
                //where transform(point) is in grid
                select transform(point);
       }
    }
    

    注意方法上的签名 - 我们在说“嗨,我们给你的这个列表,它绝对有双重点”,这就是允许代码这样的代码:

      HexGrid hexGrid = new HexGrid();      
      List<HexPoint> hexPointShape = new List<HexPoint>(); //Add some items
    
      //Compiles
      var rotatedShape1 = Algorithms
          .TransformShape(
         hexGrid,
         hexPointShape, 
         point => point.Rotate240())
        .ToList();
    
      //Compiles   
      var rotatedShape2 = Algorithms
          .TransformShape<HexPoint, TriPoint>(
         hexPointShape, 
         point => point.Rotate240())
        .ToList();     
    
      //Did not compile, but does now!
      var rotatedShape3 = Algorithms
          .TransformShape(
          hexPointShape, 
          point => point.Rotate240())
        .ToList();
    

    我曾经遇到一个泛型重载的情况,我有一组5个泛型接口,每个泛型接口的每个实现都根据它们进行参数化。 理论上这是一个很棒的设计,因为它意味着所有的方法参数和返回类型都是静态检查的。

    实际上,在设计一段时间后,意识到任何将这些接口作为参数的方法都必须指定所有的类型参数,我决定简单地使这些接口不是通用的,并且使用运行时转换为方法参数,而不是让编译器强制执行。

    我会建议简化设计 - 最多可能从接口中删除所有类型的参数。

    一种可能的解决方案,取决于你想要定义的算法的种类,可能是定义更多的接口,这些接口需要更少的类型参数,并且交换暴露更少的方法。

    例如:

    interface IPoint 
    {
        int X {get;}
        int Y {get;}
        // Maybe you do not need that one.
        IPoint Translate(IPoint dual);
    }
    interface IPoint<TPoint> : IPoint
        where TPoint : IPoint<TPoint>
    {
        new TPoint Translate(TPoint dual);
    }
    

    现在,您可以定义一个算法,该算法在没有关于双点类型的信息泄漏的情况下采用IPoint 。 不过要注意的是,对于同一个事物使用通用和非通用接口会使设计更加复杂。

    如果没有关于真实界面的更多信息,以及您需要如何使用它,我不知道要提出什么精确的修改。

    不要忘记,你应该平衡实现的复杂性和可读性 - 如果即使你在重写方法类型参数时遇到困难,那么也要考虑那些会使用你的对象而不写它们的人!

    链接地址: http://www.djcxy.com/p/70185.html

    上一篇: Expressing type relationships and avoiding long type parameter lists in C#

    下一篇: Sudoku solve method