Expressing type relationships and avoiding long type parameter lists in C#

I have this situation (drastically simplified):

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 */);
}

Here is typical implementation:

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

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

So on a HexGrid , the client can make a call to get a point on a dual grid, with exactly the right type:

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

So far so good; the client does not need to know anything about the type how the two points relate, all she needs to know is that on a HexGrid the method GetDualPoint returns a TriPoint .

Except...

I have a class full of generic algorithms, that operate on IGrid s, for example:

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

Now, the client suddenly has to know the little detail that the dual point of a HexPoint is a TriPoint , and we need to specify it as part of the type parameter list, even though it does not strictly matter for this algorithm:

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

Ideally, I would like to make DualPoint a "property" of the type IPoint , so that HexPoint.DualPoint is the type TriPoint .

Something that allows IGrid to look like this:

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

and the function CalcShortestPath like this

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

Of course, this is not possible, as far as I know.

But is there a way I can change my design to mimic this somehow? So that

  • It expresses the relationship between the two types
  • It prevents excessive type argument lists
  • It prevents clients having to think too hard about how the concrete type "specialises" the type parameters of the interface the type implements.

  • To give an indication of why this becomes a real problem: In my library IGrid actually has 4 type parameters, IPoint has 3, and both will potentially increase (up to 6 and 5). (Similar relations hold between most of these type parameters.)

    Explicit overloads instead of generics for the algorithms are not practical: there are 9 concrete implementations of each of IGrid and IPoint . Some algorithms operate on two types of grids, and hence have a tonne of type parameters. (The declaration of many functions are longer than the function body!)

    The mental burden was driven home when my IDE threw away all the type parameters during an automatic rename, and I had to put all the parameters back manually. It was not a mindless task; my brain was fried.


    As requested by @Iridium, an example showing when type inference fails. Obviously, the code below does not do anything; it's just to illustrate compiler behaviour.

    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();
       }
    }
    

    So, gonna throw up an answer based on the throwaway idea I talked about in the comments...

    The basic gist was "Define a type that conveys this concept of point duality, and use that in your relevant signatures so as to give the compiler the hints it needs"

    One thing you should read whenever you hit the dreaded "Type cannot be inferred from usage" error: http://blogs.msdn.com/b/ericlippert/archive/2009/12/10/constraints-are-not-part-of-the-signature.aspx

    In that, Messr. Lippert spells out the harsh truth that only the parameters of the signature are checked during this inference stage, NOT the constraints. So we have to be a tiny bit more "specific" here.

    First, let's define our "duality relationship" - I should note this is one way to set up these relationships, there are (in theory) an infinite variety of them.

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

    Now we go back and retrofit our existing signatures:

    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();} 
    }
    

    And likewise on the "secondary types", the grids:

    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();
        }
    }
    

    And finally on our utility methods:

    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);
       }
    }
    

    Note the signature on the method - we are saying "Hey, this list of things we're giving you, it absolutely has dual points", which is what's going to allow code like so:

      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();
    

    I once hit a case of generics overload where I had a set of 5 generic interfaces, and each of them was parametrized according to each of the implementations of the generic interfaces. That was a wonderful design in theory, as it meant that all method parameters and return types were checked statically.

    In practice, after wrangling with that design for a while, and realizing that it meant that any method that took any of these interfaces as an argument would have to specify all of the type parameters, I decided to simply make these interfaces non generic and use runtime casts for method parameters instead of having the compiler enforce it.

    I would suggest simplifying the design - up to, possibly, dropping all type parameters from the interfaces.

    A possible solution, depending on the kind of algorithms you want to define, could be to define additional interfaces, that take less type parameters, and in exchange expose less methods.

    eg:

    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);
    }
    

    Now, you can define an algorithm that takes an IPoint without having the information about the dual point type leak through. Note however that having generic and non-generic interfaces for the same thing can make the design even more complex.

    Without more information about the real interfaces, and how you need to use it, I do not really know what precise modifications to suggest.

    Do not forget that you should balance implementation complexity and readability - if even you have trouble rewriting the method type parameters, then think about people that will use your objects without having written them!

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

    上一篇: 为代数数据类型定义TH Lift实例

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