Odpowiednik typedef w C #

326

Czy w C # istnieje odpowiednik typedef, czy jakoś uzyskać podobne zachowanie? Zrobiłem trochę google, ale wszędzie wyglądam negatywnie. Obecnie mam sytuację podobną do następującej:

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

Teraz naukowiec od rakiet nie musi zorientować się, że może to bardzo szybko doprowadzić do wielu pisania (przepraszam za okropną grę słów) podczas próby zaimplementowania modułu obsługi tego zdarzenia. Ostatecznie byłoby to mniej więcej tak:

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

Tyle że w moim przypadku użyłem już złożonego typu, a nie tylko int. Byłoby miło, gdyby można to trochę uprościć ...

Edycja: tj. być może należy wpisać typ EventHandler zamiast konieczności jego redefiniowania, aby uzyskać podobne zachowanie.

Matthew Scharley
źródło

Odpowiedzi:

341

Nie, nie ma prawdziwego odpowiednika typedef. Możesz użyć dyrektyw „za pomocą” w jednym pliku, np

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

ale wpłynie to tylko na ten plik źródłowy. W C i C ++ moje doświadczenie jest takie, że typedefzwykle jest używane w plikach .h, które są szeroko zawarte - więc jednego typedefmożna użyć w całym projekcie. Ta umiejętność nie istnieje w języku C #, ponieważ w języku C # nie ma żadnej #includefunkcji, która pozwalałaby na włączenie usingdyrektyw z jednego pliku do drugiego.

Na szczęście podany przykład zawiera niejawną konwersję grupy metod. Możesz zmienić linię subskrypcji wydarzeń na:

gcInt.MyEvent += gcInt_MyEvent;

:)

Jon Skeet
źródło
11
Zawsze zapominam, że możesz to zrobić. Może dlatego, że Visual Studio sugeruje bardziej pełną wersję. Ale nie mam nic przeciwko dwukrotnemu naciśnięciu klawisza TAB zamiast wpisywania nazwy modułu obsługi;)
OregonGhost
11
Z mojego doświadczenia (którego jest mało), musisz podać w pełni kwalifikowaną nazwę typu, na przykład: using MyClassDictionary = System.Collections.Generic.Dictionary<System.String, MyNamespace.MyClass>; Czy jest poprawna? W przeciwnym razie wydaje się, że nie bierze pod uwagę usingpowyższych definicji.
tunnuz
3
Nie mogłem przekonwertować typedef uint8 myuuid[16];za pomocą dyrektywy „using”. using myuuid = Byte[16];nie kompiluje się. usingmoże być używany tylko do tworzenia aliasów typów . typedefwydaje się być znacznie bardziej elastyczny, ponieważ może utworzyć alias dla całej deklaracji (w tym rozmiarów tablicy). Czy w tym przypadku jest jakaś alternatywa?
natenho
2
@natenho: Nie bardzo. Prawdopodobnie najbliższym rozwiązaniem byłoby posiadanie struktury z buforem o stałej wielkości.
Jon Skeet
1
@tunnuz Chyba że określisz to w przestrzeni nazw
John Smith
38

Jon naprawdę dał dobre rozwiązanie, nie wiedziałem, że możesz to zrobić!

Czasem uciekałem się po dziedziczeniu od klasy i tworzeniu jej konstruktorów. Na przykład

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

Nie jest to najlepsze rozwiązanie (chyba że twój zespół zostanie wykorzystany przez inne osoby), ale działa.

Jonathan C. Dickinson
źródło
41
Zdecydowanie dobra metoda, ale należy pamiętać, że te (denerwujące) zapieczętowane typy istnieją i nie będą tam działać. Naprawdę chciałbym, żeby C # już wprowadził typedefs. To rozpaczliwa potrzeba (szczególnie dla programistów C ++).
MasterMastic
1
Stworzyłem projekt dla tej sytuacji o nazwie LikeType, który otacza typ bazowy, a nie dziedziczy po nim. Będzie również niejawnie przekonwertować DO typu bazowego, więc można użyć czegoś podobnego public class FooList : LikeType<IReadOnlyList<Foo>> { ... }, a następnie używać go w dowolnym miejscu można się spodziewać IReadOnlyList<Foo>. Moja odpowiedź poniżej pokazuje więcej szczegółów.
Matt Klein
3
Nie będzie również wnioskować o typie, Foojeśli zostanie przekazany np. Do metody szablonu, która akceptuje List<T>. Przy odpowiednim typowaniu byłoby to możliwe.
Aleksei Petrenko
18

Jeśli wiesz, co robisz, możesz zdefiniować klasę z niejawnymi operatorami w celu konwersji między klasą aliasu i klasą rzeczywistą.

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

W rzeczywistości nie popieram tego i nigdy nie użyłem czegoś takiego, ale prawdopodobnie mogłoby to zadziałać w pewnych szczególnych okolicznościach.

palswim
źródło
Dzięki @palswim, przyjechałem tu szukać czegoś takiego jak „typedef string Identifier;” więc twoja sugestia może być właśnie tym, czego potrzebuję.
yoyo
6

C # obsługuje pewne odziedziczone kowariancje dla delegatów zdarzeń, więc metoda taka jak ta:

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

Może być wykorzystany do zasubskrybowania Twojego wydarzenia, nie jest wymagana jawna obsada

gcInt.MyEvent += LowestCommonHander;

Możesz nawet użyć składni lambda, a inteligencja zostanie zrobiona za Ciebie:

gcInt.MyEvent += (sender, e) =>
{
    e. //you'll get correct intellisense here
};
Keith
źródło
Poważnie potrzebuję zająć się dobrym spojrzeniem na Linq ... ale dla
przypomnienia budowałem wtedy wersję
Och, również mogę subskrybować dobrze, ale aby uzyskać argumenty zdarzenia, potrzebuję jawnej obsady, a najlepiej kodu sprawdzającego, aby być po bezpiecznej stronie.
Matthew Scharley,
9
Składnia jest poprawna, ale nie powiedziałbym, że to „składnia Linq”; raczej jest to wyrażenie lambda. Lambda to funkcja wspierająca, która sprawia, że ​​Linq działa, ale jest całkowicie od niego niezależny. Zasadniczo wszędzie tam, gdzie możesz użyć delegata, możesz użyć wyrażenia lambda.
Scott Dorman
W porządku, powinienem był powiedzieć lambda. Delegat działałby w .Net 2, ale trzeba ponownie jawnie zadeklarować zagnieżdżony typ ogólny.
Keith,
5

Myślę, że nie ma maszynopisu. Możesz zdefiniować tylko określony typ delegata zamiast ogólnego w GenericClass, tj

public delegate GenericHandler EventHandler<EventData>

To by było krótsze. Ale co z następującą sugestią:

Użyj programu Visual Studio. W ten sposób podczas pisania

gcInt.MyEvent += 

zapewnia już pełny podpis procedury obsługi zdarzeń od Intellisense. Naciśnij TAB i już tam jest. Zaakceptuj wygenerowaną nazwę procedury obsługi lub zmień ją, a następnie ponownie naciśnij klawisz TAB, aby automatycznie wygenerować kod pośrednika procedury obsługi.

OregonGhost
źródło
2
Tak, właśnie to zrobiłem, aby wygenerować przykład. Ale wracając, aby na to jeszcze raz spojrzeć PO PONAD fakt może być mylący.
Matthew Scharley,
Wiem co masz na myśli. Dlatego chciałbym, aby moje podpisy zdarzeń były krótkie lub oderwać się od rekomendacji FxCop, aby używać Generic EventHandler <T> zamiast własnego typu delegata. Ale potem trzymaj się krótkiej wersji dostarczonej przez Jona Skeeta :)
OregonGhost
2
Jeśli masz ReSharper, powie ci, że długa wersja jest przesadzona (kolorując ją na szaro), i możesz użyć „szybkiej poprawki”, aby się go pozbyć.
Roger Lipscombe,
5

Zarówno C ++, jak i C # nie mają łatwych sposobów na stworzenie nowego typu, który jest semantycznie identyczny z istniejącym typem. Uważam, że takie „typedefs” są absolutnie niezbędne dla programowania bezpiecznego dla typu i szkoda, że ​​c # nie ma ich wbudowanych. Różnica między void f(string connectionID, string username)celu void f(ConID connectionID, UserName username)jest oczywiste ...

(Możesz osiągnąć coś podobnego w C ++ dzięki wzmocnieniu w BOOST_STRONG_TYPEDEF)

Używanie dziedziczenia może być kuszące, ale ma to pewne poważne ograniczenia:

  • nie będzie działać dla typów pierwotnych
  • typ pochodny może być nadal rzutowany na typ oryginalny, tzn. możemy wysłać go do funkcji odbierającej nasz typ oryginalny, co przeczy całemu celowi
  • nie możemy wywodzić się z zapieczętowanych klas (tzn. wiele klas .NET jest zapieczętowanych)

Jedynym sposobem na osiągnięcie podobnej rzeczy w języku C # jest skomponowanie naszego typu w nowej klasie:

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

Chociaż to zadziała, jest to bardzo szczegółowe dla samego typedef. Ponadto mamy problem z serializacją (tj. Json), ponieważ chcemy serializować klasę za pomocą jej właściwości Composed.

Poniżej znajduje się klasa pomocnicza, która wykorzystuje „Ciekawie powtarzający się wzorzec szablonu”, aby uprościć to:

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

}

Dzięki Composer powyższa klasa staje się po prostu:

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

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

A ponadto SomeTypeTypeDefserializuje do Jsona w ten sam sposób, co SomeTyperobi.

Mam nadzieję że to pomoże !

Kofifus
źródło
4

Możesz użyć biblioteki Open Source i pakietu NuGet o nazwie LikeType, który stworzyłem, co zapewni ci GenericClass<int>zachowanie, którego szukasz.

Kod wyglądałby następująco:

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
źródło
czy LikeType zadziała w przypadku czegoś złożonego, jak stackoverflow.com/questions/50404586/... ? Próbowałem się z tym bawić i nie mogę uzyskać zestawu klas, który działa.
Jay Croghan,
To nie jest tak naprawdę intencja LikeTypebiblioteki. LikeTypegłównym celem jest pomoc w Primitive Obsession , i dlatego nie chce, abyś mógł ominąć zawinięty typ, tak jak to był typ owijania. Tak jak w przypadku, jeśli to zrobię, Age : LikeType<int>to jeśli moja funkcja potrzebuje an Age, chcę upewnić się, że moje osoby dzwoniące przekazują an Age, a nie an int.
Matt Klein
Biorąc to pod uwagę, myślę, że mam odpowiedź na twoje pytanie, które tam zamieszczę.
Matt Klein
3

Oto jego kod, baw się !, wybrałem to z dotNetReference wpisz polecenie „using” wewnątrz linii przestrzeni nazw 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
źródło
2

W przypadku niezapieczętowanych klas po prostu dziedzicz po nich:

public class Vector : List<int> { }

Ale dla klas zapieczętowanych można symulować zachowanie typedef za pomocą takiej klasy bazowej:

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
źródło
1

Najlepszą alternatywą dla typedeftego, co znalazłem w C #, jest using. Na przykład mogę kontrolować precyzję ruchu zmiennego za pomocą flag kompilatora za pomocą tego kodu:

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

Niestety wymaga to umieszczenia tego na górze każdego pliku, którego używasz real_t. Obecnie nie ma możliwości zadeklarowania globalnego typu przestrzeni nazw w języku C #.

Aaron Franke
źródło