Équivalent de typedef en C #

326

Existe-t-il un équivalent typedef en C #, ou d'une manière ou d'une autre pour obtenir une sorte de comportement similaire? J'ai fait quelques recherches sur Google, mais partout où je regarde, cela semble négatif. Actuellement, j'ai une situation similaire à la suivante:

class GenericClass<T> 
{
    public event EventHandler<EventData> MyEvent;
    public class EventData : EventArgs { /* snip */ }
    // ... snip
}

Maintenant, il ne faut pas un spécialiste des fusées pour comprendre que cela peut très rapidement conduire à beaucoup de dactylographie (excuses pour le jeu de mots horrible) lors de la tentative d'implémenter un gestionnaire pour cet événement. Cela finirait par ressembler à ceci:

GenericClass<int> gcInt = new GenericClass<int>;
gcInt.MyEvent += new EventHandler<GenericClass<int>.EventData>(gcInt_MyEvent);
// ...

private void gcInt_MyEvent(object sender, GenericClass<int>.EventData e)
{
    throw new NotImplementedException();
}

Sauf que dans mon cas, j'utilisais déjà un type complexe, pas seulement un int. Ce serait bien s'il était possible de simplifier un peu cela ...

Modifier: ie. peut-être en tapant le EventHandler au lieu de devoir le redéfinir pour obtenir un comportement similaire.

Matthew Scharley
la source

Réponses:

341

Non, il n'y a pas de véritable équivalent de typedef. Vous pouvez utiliser les directives «using» dans un seul fichier, par exemple

using CustomerList = System.Collections.Generic.List<Customer>;

mais cela n'affectera que ce fichier source. En C et C ++, mon expérience est typedefgénéralement utilisée dans les fichiers .h qui sont largement inclus - donc un seul typedefpeut être utilisé sur un projet entier. Cette capacité n'existe pas en C #, car il n'y a aucune #includefonctionnalité en C # qui vous permettrait d'inclure les usingdirectives d'un fichier dans un autre.

Heureusement, l'exemple que vous donnez ne une solution - conversion de groupe méthode implicite. Vous pouvez modifier votre ligne d'abonnement aux événements pour:

gcInt.MyEvent += gcInt_MyEvent;

:)

Jon Skeet
la source
11
J'oublie toujours que tu peux le faire. Peut-être parce que Visual Studio suggère la version la plus détaillée. Mais je suis d'accord pour appuyer deux fois sur TAB au lieu de taper le nom du gestionnaire;)
OregonGhost
11
D'après mon expérience (qui est rare), vous devez spécifier le nom de type complet, par exemple: using MyClassDictionary = System.Collections.Generic.Dictionary<System.String, MyNamespace.MyClass>; est-ce correct? Sinon, il ne semble pas tenir compte des usingdéfinitions ci-dessus.
tunnuz
3
Je n'ai pas pu convertir typedef uint8 myuuid[16];via la directive "using". using myuuid = Byte[16];ne compile pas. usingpeut être utilisé uniquement pour créer des alias de type . typedefsemble être beaucoup plus flexible, car il peut créer un alias pour une déclaration entière (y compris les tailles de tableau). Y a-t-il une alternative dans ce cas?
natenho
2
@natenho: Pas vraiment. Le plus proche que vous pourriez faire serait probablement d'avoir une structure avec un tampon de taille fixe.
Jon Skeet
1
@tunnuz Sauf si vous le spécifiez dans un espace de noms
John Smith
38

Jon a vraiment donné une bonne solution, je ne savais pas que tu pouvais faire ça!

Parfois, j'ai eu recours à l'héritage de la classe et à la création de ses constructeurs. Par exemple

public class FooList : List<Foo> { ... }

Ce n'est pas la meilleure solution (sauf si votre assemblage est utilisé par d'autres personnes), mais cela fonctionne.

Jonathan C Dickinson
la source
41
Certainement une bonne méthode, mais gardez à l'esprit que ces types scellés (ennuyeux) existent, et cela ne fonctionnera pas là. Je souhaite vraiment que C # introduise déjà des typedefs. C'est un besoin désespéré (en particulier pour les programmeurs C ++).
MasterMastic
1
J'ai créé un projet pour cette situation appelé LikeType qui encapsule le type sous-jacent plutôt que d'en hériter. Il convertit également implicitement TO le type sous - jacent, de sorte que vous pouvez utiliser quelque chose comme public class FooList : LikeType<IReadOnlyList<Foo>> { ... }, puis l' utiliser partout où vous attendez un IReadOnlyList<Foo>. Ma réponse ci-dessous montre plus de détails.
Matt Klein
3
Il ne déduira pas non plus le type Foos'il est transmis par exemple à une méthode de modèle qui accepte List<T>. Avec un typedef correct, ce serait possible.
Aleksei Petrenko
18

Si vous savez ce que vous faites, vous pouvez définir une classe avec des opérateurs implicites pour convertir entre la classe d'alias et la classe réelle.

class TypedefString // Example with a string "typedef"
{
    private string Value = "";
    public static implicit operator string(TypedefString ts)
    {
        return ((ts == null) ? null : ts.Value);
    }
    public static implicit operator TypedefString(string val)
    {
        return new TypedefString { Value = val };
    }
}

Je n'approuve pas vraiment cela et je n'ai jamais utilisé quelque chose comme ça, mais cela pourrait probablement fonctionner pour certaines circonstances spécifiques.

palswim
la source
Merci @palswim, je suis arrivé ici à la recherche de quelque chose comme "identificateur de chaîne typedef;" donc votre suggestion peut être exactement ce dont j'ai besoin.
yoyo
6

C # prend en charge une certaine covariance héritée pour les délégués d'événements, donc une méthode comme celle-ci:

void LowestCommonHander( object sender, EventArgs e ) { ... } 

Peut être utilisé pour vous abonner à votre événement, aucun casting explicite requis

gcInt.MyEvent += LowestCommonHander;

Vous pouvez même utiliser la syntaxe lambda et l'intellisense sera tout fait pour vous:

gcInt.MyEvent += (sender, e) =>
{
    e. //you'll get correct intellisense here
};
Keith
la source
J'ai vraiment besoin de me déplacer pour jeter un coup d'œil à Linq ... pour mémoire cependant, je construisais pour 2.0 à l'époque (dans VS 2008 cependant)
Matthew Scharley
Oh, aussi, je peux m'abonner correctement, mais pour obtenir les arguments de l'événement, j'ai besoin d'un cast explicite, et de préférence du code de vérification de type, juste pour être sûr.
Matthew Scharley
9
La syntaxe est correcte, mais je ne dirais pas que c'est "syntaxe Linq"; c'est plutôt une expression lambda. Les lambdas sont une fonctionnalité de support qui fait fonctionner Linq, mais en sont complètement indépendants. Essentiellement, partout où vous pouvez utiliser un délégué, vous pouvez utiliser une expression lambda.
Scott Dorman
Bon point, j'aurais dû dire lambda. Un délégué fonctionnerait dans .Net 2, mais vous auriez besoin de déclarer explicitement à nouveau le type générique imbriqué.
Keith
5

Je pense qu'il n'y a pas de typedef. Vous ne pouvez définir qu'un type de délégué spécifique au lieu du générique dans GenericClass, c'est-à-dire

public delegate GenericHandler EventHandler<EventData>

Cela le raccourcirait. Mais qu'en est-il de la suggestion suivante:

Utilisez Visual Studio. De cette façon, lorsque vous avez tapé

gcInt.MyEvent += 

il fournit déjà la signature complète du gestionnaire d'événements d'Intellisense. Appuyez sur TAB et c'est là. Acceptez le nom du gestionnaire généré ou modifiez-le, puis appuyez à nouveau sur TAB pour générer automatiquement le stub du gestionnaire.

OregonGhost
la source
2
Oui, c'est ce que j'ai fait pour générer l'exemple. Mais revenir pour le revoir APRÈS le fait peut encore être déroutant.
Matthew Scharley
Je vois ce que tu veux dire. C'est pourquoi j'aime garder mes signatures d'événements courtes, ou m'éloigner de la recommandation FxCop d'utiliser Generic EventHandler <T> au lieu de mon propre type de délégué. Mais alors, restez avec la version abrégée fournie par Jon Skeet :)
OregonGhost
2
Si vous avez ReSharper, il vous dira que la version longue est exagérée (en la coloriant en gris), et vous pouvez utiliser une "solution rapide" pour vous en débarrasser à nouveau.
Roger Lipscombe
5

C ++ et C # manquent tous deux de moyens simples pour créer un nouveau type qui est sémantiquement identique à un type existant. Je trouve ces 'typedefs' totalement essentiels pour une programmation sécurisée et c'est vraiment dommage que c # ne les ait pas intégrés. La différence entre void f(string connectionID, string username)to void f(ConID connectionID, UserName username)est évidente ...

(Vous pouvez réaliser quelque chose de similaire en C ++ avec boost dans BOOST_STRONG_TYPEDEF)

Il peut être tentant d'utiliser l'héritage mais cela a quelques limitations majeures:

  • cela ne fonctionnera pas pour les types primitifs
  • le type dérivé peut toujours être casté dans le type d'origine, c'est-à-dire que nous pouvons l'envoyer à une fonction recevant notre type d'origine, ce qui va à l'encontre de l'objectif
  • nous ne pouvons pas dériver de classes scellées (et donc de nombreuses classes .NET sont scellées)

La seule façon de réaliser une chose similaire en C # est de composer notre type dans une nouvelle classe:

Class SomeType { 
  public void Method() { .. }
}

sealed Class SomeTypeTypeDef {
  public SomeTypeTypeDef(SomeType composed) { this.Composed = composed; }

  private SomeType Composed { get; }

  public override string ToString() => Composed.ToString();
  public override int GetHashCode() => HashCode.Combine(Composed);
  public override bool Equals(object obj) => obj is TDerived o && Composed.Equals(o.Composed); 
  public bool Equals(SomeTypeTypeDefo) => object.Equals(this, o);

  // proxy the methods we want
  public void Method() => Composed.Method();
}

Bien que cela fonctionne, il est très détaillé pour un typedef. De plus, nous avons un problème avec la sérialisation (ie vers Json) car nous voulons sérialiser la classe via sa propriété Composed.

Vous trouverez ci-dessous une classe d'assistance qui utilise le «modèle de modèle curieusement récurrent» pour simplifier les choses:

namespace Typedef {

  [JsonConverter(typeof(JsonCompositionConverter))]
  public abstract class Composer<TDerived, T> : IEquatable<TDerived> where TDerived : Composer<TDerived, T> {
    protected Composer(T composed) { this.Composed = composed; }
    protected Composer(TDerived d) { this.Composed = d.Composed; }

    protected T Composed { get; }

    public override string ToString() => Composed.ToString();
    public override int GetHashCode() => HashCode.Combine(Composed);
    public override bool Equals(object obj) => obj is Composer<TDerived, T> o && Composed.Equals(o.Composed); 
    public bool Equals(TDerived o) => object.Equals(this, o);
  }

  class JsonCompositionConverter : JsonConverter {
    static FieldInfo GetCompositorField(Type t) {
      var fields = t.BaseType.GetFields(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.FlattenHierarchy);
      if (fields.Length!=1) throw new JsonSerializationException();
      return fields[0];
    }

    public override bool CanConvert(Type t) {
      var fields = t.GetFields(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.FlattenHierarchy);
      return fields.Length == 1;
    }

    // assumes Compositor<T> has either a constructor accepting T or an empty constructor
    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) {
      while (reader.TokenType == JsonToken.Comment && reader.Read()) { };
      if (reader.TokenType == JsonToken.Null) return null; 
      var compositorField = GetCompositorField(objectType);
      var compositorType = compositorField.FieldType;
      var compositorValue = serializer.Deserialize(reader, compositorType);
      var ctorT = objectType.GetConstructor(new Type[] { compositorType });
      if (!(ctorT is null)) return Activator.CreateInstance(objectType, compositorValue);
      var ctorEmpty = objectType.GetConstructor(new Type[] { });
      if (ctorEmpty is null) throw new JsonSerializationException();
      var res = Activator.CreateInstance(objectType);
      compositorField.SetValue(res, compositorValue);
      return res;
    }

    public override void WriteJson(JsonWriter writer, object o, JsonSerializer serializer) {
      var compositorField = GetCompositorField(o.GetType());
      var value = compositorField.GetValue(o);
      serializer.Serialize(writer, value);
    }
  }

}

Avec Composer, la classe ci-dessus devient simplement:

sealed Class SomeTypeTypeDef : Composer<SomeTypeTypeDef, SomeType> {
   public SomeTypeTypeDef(SomeType composed) : base(composed) {}

   // proxy the methods we want
   public void Method() => Composed.Method();
}

Et en plus la SomeTypeTypeDefvolonté sérialisera à Json de la même manière qui SomeTypefait.

J'espère que cela t'aides !

kofifus
la source
4

Vous pouvez utiliser une bibliothèque open source et un package NuGet appelé LikeType que j'ai créé qui vous donneront le GenericClass<int>comportement que vous recherchez.

Le code ressemblerait à:

public class SomeInt : LikeType<int>
{
    public SomeInt(int value) : base(value) { }
}

[TestClass]
public class HashSetExample
{
    [TestMethod]
    public void Contains_WhenInstanceAdded_ReturnsTrueWhenTestedWithDifferentInstanceHavingSameValue()
    {
        var myInt = new SomeInt(42);
        var myIntCopy = new SomeInt(42);
        var otherInt = new SomeInt(4111);

        Assert.IsTrue(myInt == myIntCopy);
        Assert.IsFalse(myInt.Equals(otherInt));

        var mySet = new HashSet<SomeInt>();
        mySet.Add(myInt);

        Assert.IsTrue(mySet.Contains(myIntCopy));
    }
}
Matt Klein
la source
LikeType fonctionnerait-il pour quelque chose de complexe comme stackoverflow.com/questions/50404586/… ? J'ai essayé de jouer avec et je n'arrive pas à obtenir une configuration de classe qui fonctionne.
Jay Croghan
Ce n'est pas vraiment l'intention de la LikeTypebibliothèque. LikeTypeLe but principal de 'est d'aider à l' obsession primitive , et en tant que tel, il ne veut pas que vous puissiez passer le type encapsulé comme s'il s'agissait du type encapsuleur. Comme dans, si je fais Age : LikeType<int>alors si ma fonction a besoin d'un Age, je veux m'assurer que mes appelants passent un Age, pas un int.
Matt Klein
Cela dit, je pense avoir une réponse à votre question, que je posterai là-bas.
Matt Klein
3

Voici le code pour cela, profitez-en!, Je l'ai repris du type dotNetReference, l'instruction "using" à l'intérieur de la ligne d'espace de noms 106 http://referencesource.microsoft.com/#mscorlib/microsoft/win32/win32native.cs

using System;
using System.Collections.Generic;
namespace UsingStatement
{
    using Typedeffed = System.Int32;
    using TypeDeffed2 = List<string>;
    class Program
    {
        static void Main(string[] args)
        {
        Typedeffed numericVal = 5;
        Console.WriteLine(numericVal++);

        TypeDeffed2 things = new TypeDeffed2 { "whatever"};
        }
    }
}
shakram02
la source
2

Pour les classes non scellées, héritez-en simplement:

public class Vector : List<int> { }

Mais pour les classes scellées, il est possible de simuler le comportement typedef avec une telle classe de base:

public abstract class Typedef<T, TDerived> where TDerived : Typedef<T, TDerived>, new()
{
    private T _value;

    public static implicit operator T(Typedef<T, TDerived> t)
    {
        return t == null ? default : t._value;
    }

    public static implicit operator Typedef<T, TDerived>(T t)
    {
        return t == null ? default : new TDerived { _value = t };
    }
}

// Usage examples

class CountryCode : Typedef<string, CountryCode> { }
class CurrencyCode : Typedef<string, CurrencyCode> { }
class Quantity : Typedef<int, Quantity> { }

void Main()
{
    var canadaCode = (CountryCode)"CA";
    var canadaCurrency = (CurrencyCode)"CAD";
    CountryCode cc = canadaCurrency;        // Compilation error
    Concole.WriteLine(canadaCode == "CA");  // true
    Concole.WriteLine(canadaCurrency);      // CAD

    var qty = (Quantity)123;
    Concole.WriteLine(qty);                 // 123
}
Vlad Rudenko
la source
1

La meilleure alternative à typedefcelle que j'ai trouvée en C # est using. Par exemple, je peux contrôler la précision du flotteur via les drapeaux du compilateur avec ce code:

#if REAL_T_IS_DOUBLE
using real_t = System.Double;
#else
using real_t = System.Single;
#endif

Malheureusement, cela nécessite que vous le placiez en haut de chaque fichier où vous utilisez real_t. Il n'existe actuellement aucun moyen de déclarer un type d'espace de noms global en C #.

Aaron Franke
la source