Эквивалент typedef в C #

326

Есть ли в C # эквивалент typedef или как-то похожее поведение? Я немного погуглил, но везде я выгляжу негативно. В настоящее время у меня есть ситуация, похожая на следующую:

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

Теперь не нужно быть ученым-ракетчиком, чтобы понять, что это может очень быстро привести к значительным наборам (извинения за ужасный каламбур) при попытке реализовать обработчик для этого события. В конечном итоге это будет примерно так:

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

За исключением того, что в моем случае я уже использовал сложный тип, а не просто int. Было бы хорошо, если бы можно было немного упростить это ...

Редактировать: т.е. возможно, определите EventHandler вместо необходимости переопределять его, чтобы получить похожее поведение.

Мэтью Шарли
источник

Ответы:

341

Нет, истинного эквивалента typedef нет. Вы можете использовать директивы using в одном файле, например

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

но это повлияет только на этот исходный файл. В C и C ++ мой опыт typedefпоказывает, что обычно он используется в файлах .h, которые широко включены - так что один typedefможно использовать для всего проекта. Эта возможность не существует в C #, потому что #includeв C # нет функциональности, которая позволила бы вам включать usingдирективы из одного файла в другой.

К счастью, пример вы даете действительно есть FIX - неявное преобразование метод групповой. Вы можете изменить строку подписки на событие просто:

gcInt.MyEvent += gcInt_MyEvent;

:)

Джон Скит
источник
11
Я всегда забываю, что ты можешь сделать это. Возможно, потому что Visual Studio предлагает более подробную версию. Но я хорошо
нажимаю
11
По моему опыту (которого мало), вы должны указать полное имя типа, например: using MyClassDictionary = System.Collections.Generic.Dictionary<System.String, MyNamespace.MyClass>; это правильно? В противном случае он, кажется, не учитывает usingопределения над ним.
Tunnuz
3
Я не мог конвертировать typedef uint8 myuuid[16];с помощью директивы using. using myuuid = Byte[16];не компилируется usingможет использоваться только для создания псевдонимов типов . typedefкажется гораздо более гибким, поскольку он может создать псевдоним для всего объявления (включая размеры массива). Есть ли альтернатива в этом случае?
natenho
2
@natenho: Не совсем. Вероятно, самое близкое к этому - иметь структуру с буфером фиксированного размера.
Джон Скит
1
@tunnuz Если вы не укажете это в пространстве имен
Джон Смит
38

Джон действительно дал хорошее решение, я не знал, что ты сможешь это сделать!

Временами я прибегал к наследованию от класса и созданию его конструкторов. Например

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

Не лучшее решение (если ваша сборка не используется другими людьми), но оно работает.

Джонатан С Дикинсон
источник
41
Определенно хороший метод, но имейте в виду, что эти (раздражающие) запечатанные типы существуют, и они не будут работать там. Я действительно хотел бы, чтобы C # уже ввел typedefs. Это отчаянная необходимость (особенно для программистов на C ++).
MasterMastic
1
Я создал проект для этой ситуации под названием LikeType, который обертывает базовый тип, а не наследует от него. Он также будет неявно преобразовать TO базового типа, так что вы могли бы использовать что - то вроде , public class FooList : LikeType<IReadOnlyList<Foo>> { ... }а затем использовать его в любом месте вы бы ожидать IReadOnlyList<Foo>. Мой ответ ниже показывает больше деталей.
Мэтт Кляйн
3
Он также не будет выводить тип, Fooесли передан, например, шаблонному методу, который принимает List<T>. При правильном typedef это было бы возможно.
Алексей Петренко
18

Если вы знаете, что делаете, вы можете определить класс с неявными операторами для преобразования между классом псевдонима и реальным классом.

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

Я на самом деле не одобряю это и никогда не использовал что-то подобное, но это могло бы сработать для некоторых конкретных обстоятельств.

palswim
источник
Спасибо @palswim, я нашел здесь что-то вроде "typedef string Identifier;" так что ваше предложение может быть именно то, что мне нужно.
yoyo
6

C # поддерживает некоторую унаследованную ковариацию для делегатов событий, поэтому такой метод:

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

Может быть использован для подписки на ваше событие, не требуется явное приведение

gcInt.MyEvent += LowestCommonHander;

Вы даже можете использовать лямбда-синтаксис, и intellisense все будет сделано для вас:

gcInt.MyEvent += (sender, e) =>
{
    e. //you'll get correct intellisense here
};
Кит
источник
Мне действительно нужно хорошенько взглянуть на Linq ... для протокола, хотя я в то время
собирал
О, также, я могу подписаться нормально, но затем, чтобы получить аргументы события, мне нужно явное приведение, и предпочтительно код проверки типа, просто чтобы быть в безопасности.
Мэтью Шарли
9
Синтаксис правильный, но я бы не сказал, что это «синтаксис Linq»; скорее это лямбда-выражение. Лямбды - это вспомогательная функция, которая заставляет Linq работать, но абсолютно не зависит от нее. По сути, везде, где вы можете использовать делегата, вы можете использовать лямбда-выражение.
Скотт Дорман
Справедливо, я должен был сказать лямбду. Делегат будет работать в .Net 2, но вам нужно будет снова явно объявить вложенный универсальный тип.
Кит
5

Я думаю, что нет typedef. Вы можете определить только определенный тип делегата вместо общего в GenericClass, т.е.

public delegate GenericHandler EventHandler<EventData>

Это сделало бы это короче. Но как насчет следующего предложения:

Используйте Visual Studio. Таким образом, когда вы набрали

gcInt.MyEvent += 

он уже предоставляет полную подпись обработчика событий от Intellisense. Нажмите клавишу TAB, и она там. Примите имя сгенерированного обработчика или измените его, а затем снова нажмите клавишу TAB, чтобы автоматически сгенерировать заглушку обработчика.

OregonGhost
источник
2
Да, это то, что я сделал, чтобы создать пример. Но возвращаясь, чтобы посмотреть на это снова, ПОСЛЕ того, как факт все еще может сбивать с толку.
Мэтью Шарли
Я знаю, что Вы имеете ввиду. Вот почему я хотел бы, чтобы мои подписи событий были короткими, или избегал рекомендации FxCop использовать Generic EventHandler <T> вместо моего собственного типа делегата. Но затем придерживайтесь сокращенной версии, предоставленной Джоном
Скитом
2
Если у вас есть ReSharper, он скажет вам, что длинная версия излишня (если закрасить ее в серый), и вы можете использовать «быстрое исправление», чтобы избавиться от нее снова.
Роджер Липскомб
5

И в C ++, и в C # отсутствуют простые способы создания нового типа, который семантически идентичен существующему типу. Я нахожу такие 'typedefs' совершенно необходимыми для безопасного программирования типов, и это настоящий позор, что c # не имеет их встроенных. Разница между void f(string connectionID, string username)до void f(ConID connectionID, UserName username)очевидна ...

(Вы можете добиться чего-то подобного в C ++ с помощью boost в BOOST_STRONG_TYPEDEF)

Может быть заманчиво использовать наследование, но это имеет некоторые серьезные ограничения:

  • это не будет работать для примитивных типов
  • производный тип все еще может быть приведен к исходному типу, т. е. мы можем отправить его функции, получающей наш исходный тип, что отрицательно сказывается на цели
  • мы не можем наследовать от запечатанных классов (т.е. многие классы .NET запечатаны)

Единственный способ добиться подобного в C # - это создать наш тип в новом классе:

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

Хотя это сработает, это очень многословно только для typedef. Кроме того, у нас есть проблема с сериализацией (то есть с Json), поскольку мы хотим сериализовать класс через его свойство Composed.

Ниже приведен вспомогательный класс, который использует «шаблон любопытно повторяющегося шаблона», чтобы сделать это намного проще:

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

}

С Composer вышеупомянутый класс становится просто:

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

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

Кроме того, он SomeTypeTypeDefбудет сериализован в Json так же, как и в SomeTypeслучае с ним.

Надеюсь это поможет !

kofifus
источник
4

Вы можете использовать библиотеку с открытым исходным кодом и пакет NuGet под названием LikeType, который я создал и который даст вам GenericClass<int>поведение, которое вы ищете.

Код будет выглядеть так:

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));
    }
}
Мэтт Кляйн
источник
хотел бы, чтобы LikeType работал для чего-то сложного, например stackoverflow.com/questions/50404586/… ? Я пытался поиграть с ним и не могу получить настройку класса, которая работает.
Джей Кроган
Это не совсем цель LikeTypeбиблиотеки. LikeTypeОсновная цель - помочь с Primitive Obsession , и поэтому он не хочет, чтобы вы могли передавать обернутый тип, как будто это был тип обертки. Например, если я сделаю Age : LikeType<int>то, если моя функция нуждается в Age, я хочу убедиться, что мои абоненты передают Age, а не int.
Мэтт Кляйн
При этом, я думаю, у меня есть ответ на ваш вопрос, который я опубликую там.
Мэтт Кляйн
3

Вот код для этого, наслаждайтесь! Я взял это из типа dotNetReference выражение «using» в строке пространства имен 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
источник
2

Для незапечатанных классов просто наследуйте от них:

public class Vector : List<int> { }

Но для закрытых классов можно моделировать поведение typedef с таким базовым классом:

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
}
Влад Руденко
источник
1

Лучшая альтернатива тому, typedefчто я нашел в C # - это using. Например, я могу контролировать точность с плавающей точкой с помощью флагов компилятора с помощью этого кода:

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

К сожалению, это требует, чтобы вы поместили это вверху каждого файла, где вы используете real_t. В настоящее время нет способа объявить тип глобального пространства имен в C #.

Аарон Франке
источник