C# abstract struct

2020-03-11 10:11发布

问题:

How can I achieve inheritance (or similar) with structs in C#? I know that an abstract struct isn't possible, but I need to achieve something similar.

I need it as a struct because it has to be a value type. And I need inheritance because I need a generic array and methods that I can garantee are there.

I think an example will go a long way, so heres some code that won't compile, but shows what I want to achieve;

    abstract struct Vertex
    {
       abstract int SizeInBytes;
       abstract void SetPointers();
    }
    struct ColorVertex : Vertex
    {
       Vector3 Position;
       Vector4 Color;

       override int SizeInBytes //static
       {
          get { return (3 + 4) * 4; }
       }
       override void SetVertexPointers() //static
       {
           ...
       }
    }

class main
{
   static void main()
   {
      Vertex[] verts = new Vertex[3];
      for(int i = 0; i < 3; i++)
          verts[i] = new ColorVertex();

      verts[0].SetVertexPointers(); //should call ColorVertex.SetVertexPointers

      externalAPIcall(verts);
   }
}

EDIT:

The reason I need value types is so that I can make an array of them, and pass it to OpenGL as a vertex buffer. For this, the data needs to be directly contained in this array.

I'd be surprised if this were simply not possible to achieve.

回答1:

In light of your recent edit:

The reason I need value types is so that I can make an array of them, and pass it to OpenGL as a vertex buffer. For this, the data needs to be directly contained in this array.

It seems like the real solution for you is encapsulation. If the layout of your struct is dictated by a third-party API (so that you can interact with unmanaged code), then you should really consider wrapping the API types in appropriate classes rather than trying to interact with them directly in code.

For one, you state this:

I need it as a struct because it has to be a value type. And I need inheritance because I need a generic array and methods that I can garantee are there.

This won't turn out the way you're expecting. As others have pointed out, the only way to define a set of common functionality that can apply to structs is through interfaces (for example, the primitive types in .NET implement IComparable). Unfortunately, if you were to declare an array of type IYourInterface, all of the values will get boxed (interface references are reference types, even if the underlying value they're pointing to are value types).

For example, let's say you declare an IVertex interface:

public interface IVertex
{
    int SizeInBytes { get; }
    void SetPointers();
}

And you have one or more value types that implement it:

struct ColorVertex : IVertex
{
   Vector3 Position;
   Vector4 Color;

   override int SizeInBytes //static
   {
      get { return (3 + 4) * 4; }
   }
   override void SetVertexPointers() //static
   {
       ...
   }
}

Whenever you do this:

ColorVertex myVertex = new ColorVertex();

IVertex foo = myVertex;

The second line will box the value of myVertex and store a reference to that boxed value in foo. Since arrays are just a series of variables, the same rules apply:

IVertex[] foos = { myVertex };

All of the values in foos will be boxed, and their references stored. This is different than if you did:

ColorVertex[] colors = { myVertex };

Where no boxing is necessary.

This has implications directly related to what you're seeking, as boxing the values now means that you no longer have a contiguous block of values (well, you do, but the contiguous block is just references; the values themselves lie elsewhere).

Encapsulation

Given the fact that you

  1. Have a third-party API with a defined type that you need to interact with
  2. The requirement to support different use cases in your code and wish to use object-oriented design patterns to do so

You should really consider wrapping the OpenGL API. For example, let's say that you have the following:

// OpenGL type
struct Vertex
{
    int SizeInBytes;
}

public static extern void OpenGLFunction(Vertex[] vertices);

What is likely a better option would be to define your own interface, then hide the OpenGL API:

public abstract class VertexBase
{
    internal Vertex ToVertex()
    {
        // your logic here
    }
}

public static class OpenGL
{
    public static void WrappedFunction(VertexBase[] vertices)
    {
        Vertex[] outVertices = new Vertex[vertices.Length];

        for(int i = 0; i < vertices.Length; i++)
        {
            outVertices[i] = vertices[i].ToVertex();
        }

        OpenGLFunction(outVertices);
    }
}

(This is obviously a contrived example, but it should demonstrate what I'm trying to get across in terms of introducing a layer of abstraction between your code and the other API)



回答2:

In C#, you can use interfaces to achieve something akin to polymorphism with value types (structs) as you can't derive directly from a struct but you can have multiple struct types implement specific interfaces.

Therefore, instead of your abstract struct, Vertex, you can have an interface, IVertex.

interface IVertex
{
    int SizeInBytes { get; }
    void SetPointers();
}

However, it is exceedingly rare that you need to implement your own value types, so make sure you really need value type semantics before proceeding. If you do implement your own value types, make sure they're immutable as mutable value types are a gateway to all kinds of horrible problems.

You should be aware that boxing will occur when casting from a value type to an interface. Not only does this have implications if your value types are mutable (don't make mutable value types), but this will decrease, or most likely cancel out any memory advantage you may gain from using a value type, depending on when or how you do this and whether you do it for every value - use a profiler if you're unsure.



回答3:

You can't, basically. You can't derive from a struct. Why do you think you want a struct instead of a class? You say "it has to be a value type" - why? Likewise do you think inheritance is the only option instead of (say) composition? For example, you could use:

public struct Vertex
{
    // ...
}

public struct Color
{
    // ...
}

public struct ColorVertex
{
    private readonly Color color;
    private readonly Vertex vertex;

    // ...
}

You're simply not going to be able to get an "abstract struct" or anything similar to work, so I suggest you explain the reasons behind your unsatisfiable requirements, instead of just stating them as requirements which can't be avoided.



回答4:

One under-appreciated feature of value types in .net is that they can be passed as interface-constrained generic types without boxing. For example:

T returnSmaller<T>(ref T p1, ref T p2) where T:IComparable<T>
{
  return p1.CompareTo(p2) < 0 ? p1 : p2;
}

Note that I used ref parameters to eliminate making extra temporary copies of the two parameters; an extra copy of p2 will end up being made when they are passed to the CompareTo method, and at least one extra copy will likely be made when the result is returned, but making two redundant copies would be better than making four. In any case, the above method may be invoked without boxing on any type T which implements IComparable<T>.

Unfortunately, there's no terribly nice way of saying "if T is one type of struct, pass it to some method which takes that type; otherwise if it's some other type, pass it to a method and take that one". Thus, code which will require a specific exact class (like the code using the API's) will likely have to be non-generic. Nonetheless, if there are some methods which should be usable on a variety of structs, having those structs implement interfaces and then passing them as constrained generic types may offer some huge advantages.



回答5:

You can use interfaces

interface IVertex 
{
    int SizeInBytes();
    void SetPointers();
}

struct ColorVertex : IVertex
{
   Vector3 Position;
   Vector4 Color;

   int SizeInBytes
   {
      get { return (3 + 4) * 4; }
   }
   void SetVertexPointers()
   {
       ...
   }
}


回答6:

It looks like what you want is an interface.

public interface IVertex
{
    int SizeInBytes { get; }
    void SetPointers();
}

public struct ColorVertex : IVertex
{
   private Vector3 Position;
   private Vector4 Color;

   public int SizeInBytes
   {
      get { return (3 + 4) * 4; }
   }

   public void SetVertexPointers() // Did you mean SetPointers?
   {
   }
}

An interface makes sense since all of your methods are declared abstract (this would mean it relies on deriving classes to implement the method which is essentially what an interface is)



回答7:

You can create interface IVertex and then add to Your structs.



回答8:

Maybe you can use a "union" type:

enum VertexType : byte { 
  Vertex,
  ColorVertex
}

[StructLayout(LayoutKind.Explicit)]
struct Vertex {

  [FieldOffset(0)]
  public readonly VertexType Type;

  [FieldOffset(1)]
  public readonly int SizeInBytes;

  public Vertex(VertexType type /* other necessary parameters... */) {
    Type = type;
    // set default values for fields...
    switch (Type) {
      // set the fields for each type:
      case VertexType.ColorVertex: 
        SizeInBytes = (3 + 4) * 4; 
        // other fields ...
        break;
      default: 
        SizeInBytes = 2; // or whatever...
        // other fields ...
        break;
    }
  }

  // other fields with overlapping offsets for the different vertex types...

}

Just remember to make it immutable and to access only the fields that make sense for each type.



回答9:

Classes can be "value types" as well, (in the sense used in Domain Driven Design). All you have to do is make it immutable, make the constructors inaccessible publicly (Protected or internal), and create static factory methods to create instances of them and control their instantiation, and do not have any setters on your properties...

NOTE: The phrase Value Type in this contect has nothing to do with Value type vs Reference Type. It has to do with Value Type vs Entity Type as used in Domain Drtiven Design or Domain Modeling...