简体   繁体   中英

C# class instance attribute mechanism

Is there a sane way in C# to achieve the following construct (in pseudocode):

void Method(MyClass<Attribute1, Attribute2, ...> foo)
{
  // here I am guaranteed that foo has the specified attributes
}

Where Attribute* are, for example, enum values, such that only instances of MyClass instantiated with the attributes required by the method can be passed to the method (and otherwise fail to compile)?

I tried looking at generics since I know that C++ templates can make this work so it seemed like a logical starting point, but I couldn't get it working elegantly (I tried using interfaces to constrain the types of the parameter in this fashion but it was very bulky and frankly unusable since I have at least 4 attributes).

I want to do this to avoid having lots of annoying checks at the beginning of each method. I am doing DirectX 11 graphics development so I am kind of constrained by the API which does not make it particularly easy to pass objects around in this "type-safe" manner (in DirectX every resource has a large "Description" structure which contains information about what the resource can and cannot do, is and is not, etc.. and is tedious and error-prone to parse, so I am trying to write a wrapper around it for my and my users' convenience).

I also cannot have different class types for every case because there are a lot of combinations, so this seems like the most comfortable way to write code like this, and I am hoping C# makes this possible.


I'm sure there is a name for this kind of language feature (if you know it please let me know, I would have googled but this is kind of hard to search for when you don't know the proper keywords...)

Generic type parameters in .NET must be types themselves. You can't create a generic type/method that is specific to a particular value of the Generic type parameter only.

If you do not want or cannot create a type that represents the attribute values you want your method being restricted to, you will have to do sanity checks in your method to ensure that the proper attribute values are used in the provided "foo" object.


Using specific types as representation of specific attribute values might be an answer for the problem you asked about, but it has the disadvantage of not supporting switch-case statements (see further below). Please also read the final note at the end of my answer.

Say, you want a type that represents textures. Textures can have different number of channels, and different bit depths. You could then declare a generic texture type like this:

class Texture<TChannels, TBPC>
    where TChannels : INumOfChannels,new()
    where TBPC : IBitsPerChannel,new()

INumOfChannels and IBitsPerChannel are just interfaces and can be empty.
The new() constraint prevents creation of a concrete Texture type by using the interfaces themselves.

For different channels and different BPCs, you will create empty types extending from the respective base interfaces, for example:

class FourChannels : INumOfChannels {};
class ThreeChannels : INumOfChannels {};

class BitsPerChannel_24 : IBitsPerChannel {};
class BitsPerChannel_32 : IBitsPerChannel {};

Using this, you can restrict your generic method to certain attribute combinations. In case your method should only deal with 4-channel and 32bpc textures:

void MyMethod<TChannels, TBPC>(Texture<TChannels, TBPC> foo)
    where TChannels : FourChannels
    where TBPC : BitsPerChannel_32


Now, every good thing also has dark sides. How would you do something like this (written as pseudo-code)?

switch (NumOfChannelsAttribute)
{
    case FourChannels:
        // do something
        break;

    case ThreeChannels:
        // do something else
        break;
}

You can't, at least not in an easy and simple way, because "FourChannel" and "ThreeChannel" are types, not integral values.

Of course, you can still use if constructs. For this to work you would need to implement a property in the generic texture type which provides the used attributes:

class Texture<TChannels, TBPC> where TChannels : INumOfChannels,new() where TBPC : IBitsPerChannel,new()
{
    public Type ChannelsAttribute
    {
        get { return typeof(TChannels); }
    }

    public Type BitsPerChannelAttribute
    {
        get { return typeof(TBPC); }
    }
}

In an if construct, you could utilize this as follows:

var myTex = new Texture<FourChannels, BitsPerChannel_32>();

if (myTex.ChannelsAttribute == typeof(FourChannels))
{
    ... do something with 4 channels
}
else
{
    ... though luck, only 4 channels are supported...
}


A final note and advice:
While this might work for your problem, resorting to these kind of 'tricks' usually is an indication of a flawed design. I think it is well-invested time if you revisit the design choices you made in your code, so you don't need to rely on crutches like this.

C# doesn't have such a feature. You mention you have tried using interfaces, but don't specify how. The way I'd suggest you try using them is by using generics with multiple constraints, eg

void Method(T foo) where T : IAttribute1, IAttribute2, IAttribute3, IAttribute4
{
}

Let's say one such attribute class is then ICpuMappable, then you can constrain types that can be used with Method1 with:

void Method1(T foo) where T : ICpuMappable
{
}

and you can know any foo passed to Method1 is CPU mappable.

You'll likely end up with lots of interfaces, but as many will be treated as "flags", they shouldn't be too difficult to maintain.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM