Лучшая альтернатива описанию случая - PullRequest
15 голосов
/ 21 мая 2010

В настоящее время у меня есть оператор switch, который запускает около 300 нечетных строк. Я знаю, что это не так уж сложно, но я уверен, что есть лучший способ справиться с этим.

Оператор switch принимает Enum, который используется для определения определенных свойств, относящихся к ведению журнала. В настоящее время проблема заключается в том, что очень легко пропустить значение перечисления и что ему не будет присвоено значение, которого нет в операторе switch.

Есть ли опция, которую можно использовать, чтобы убедиться, что используется каждое перечисление и задан пользовательский набор значений, необходимых для его работы?

EDIT:


Пример кода по запросу: (Это упрощенно, но показывает именно то, что я имею в виду. Также существует перечисление со значениями ниже.)

internal void GenerateStatusLog(LogAction ActionToLog)
{
    switch (ActionToLog)
    {
        case LogAction.None:
            {
                return;
            }
        case LogAction.LogThis:
            {
                ActionText = "Logging this Information";
                LogText = "Go for it.";

                break;
            }
    }

    // .. Do everything else
}

Ответы [ 6 ]

5 голосов
/ 21 мая 2010

EDIT

Я снова обдумал это, огляделся по смежным вопросам в SO и написал некоторый код. Я создал класс с именем AdvancedSwitch<T>, который позволяет добавлять случаи и предоставляет метод для оценки значения и позволяет указывать значения, которые он должен проверять на наличие.

Вот что я придумал:

public class AdvancedSwitch<T> where T : struct
{
    protected Dictionary<T, Action> handlers = new Dictionary<T, Action>();

    public void AddHandler(T caseValue, Action action)
    {
        handlers.Add(caseValue, action);
    }

    public void RemoveHandler(T caseValue)
    {
        handlers.Remove(caseValue);
    }

    public void ExecuteHandler(T actualValue)
    {
        ExecuteHandler(actualValue, Enumerable.Empty<T>());
    }

    public void ExecuteHandler(T actualValue, IEnumerable<T> ensureExistence)
    {
        foreach (var val in ensureExistence)
            if (!handlers.ContainsKey(val))
                throw new InvalidOperationException("The case " + val.ToString() + " is not handled.");

        handlers[actualValue]();
    }
}

Вы можете использовать класс следующим образом:

public enum TrafficColor { Red, Yellow, Green }

public static void Main()
{
    Console.WriteLine("Choose a traffic color: red, yellow, green?");
    var color = (TrafficColor)Enum.Parse(typeof(TrafficColor), Console.ReadLine());
    var result = string.Empty;

    // Creating the "switch"
    var mySwitch = new AdvancedSwitch<TrafficColor>();

    // Adding a single case
    mySwitch.AddHandler(TrafficColor.Green, delegate
    {
        result = "You may pass.";
    });

    // Adding multiple cases with the same action
    Action redAndYellowDelegate = delegate
    {
        result = "You may not pass.";
    };
    mySwitch.AddHandler(TrafficColor.Red, redAndYellowDelegate);
    mySwitch.AddHandler(TrafficColor.Yellow, redAndYellowDelegate);

    // Evaluating it
    mySwitch.ExecuteHandler(color, (TrafficColor[])Enum.GetValues(typeof(TrafficColor)));

    Console.WriteLine(result);
}

Благодаря творческому использованию анонимных делегатов вы можете легко добавлять новые случаи в свой «блок переключателей». :)
Не то чтобы вы также могли использовать лямбда-выражения и лямбда-блоки, например () => { ... } вместо delegate { ... }.

Вы можете легко использовать этот класс вместо длинных блоков переключателей.

Оригинальный пост:

Если вы используете Visual Studio, всегда создавайте операторы swich с фрагментом кода switch. Наберите switch, дважды нажмите клавишу Tab, и она автоматически сгенерирует все возможности для вас.

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

Я имею в виду что-то вроде этого:

switch (something)
{
    ...
    case YourEnum.SomeValue:
        ...
        break;
    default:
        throw new InvalidOperationException("Default case reached.");
}
4 голосов
/ 21 мая 2010

Ну, в случае default есть бросок ... Нет никакой другой конструкции времени редактирования / компиляции.

Однако стратегия, посетитель и другие связанные с ними шаблоны могут быть подходящими, если вы решите сделать это во время выполнения.

Пример кода поможет получить лучший ответ.

РЕДАКТИРОВАТЬ: Спасибо за образец. Я все еще думаю, что это нужно немного прояснить, так как вы не рассматриваете, есть ли некоторые параметры, которые применяются только к некоторым case s и т. Д.

Действие часто используется в качестве псевдонима для шаблона Command, и тот факт, что ваш Enum называется LogAction, означает, что каждое значение несет в себе поведение - будь то подразумеваемое (вы вставляете соответствующий код в case) или явный (в определенном классе иерархии команд).

Таким образом, мне кажется, что использование шаблона Command целесообразно (хотя ваш пример этого не доказывает) - то есть иметь класс (потенциально иерархию, использующую перегрузки конструктора или любые другие [набор] фабричных механизмов), который сохраняет состояние, связанное с запросом, а также конкретное поведение. Затем вместо передачи значения Enum создайте соответствующий экземпляр LogCommand для регистратора, который просто вызывает его (потенциально передавая «приемник» приемника журнала, в который может войти команда). В противном случае вы высовываете случайные подмножества параметров в разных местах.

SEEALSO похожие сообщения:

2 голосов
/ 21 мая 2010

Одним из возможных решений является использование SortedDictionary:

delegate void EnumHandler (args);
SortedDictionary <Enum, EnumHandler> handlers;

constructor
{
   handlers = new SortedDictionary <Enum, EnumHandler> ();
   fill in handlers
}

void SomeFunction (Enum enum)
{
  EnumHandler handler;

  if (handlers.TryGetValue (enum, out handler))
  {
     handler (args);
  }
  else
  {
    // not handled, report an error
  }
}

Этот метод позволяет динамически заменять обработчики. Вы также можете использовать List как часть значения словаря и иметь несколько обработчиков для каждого перечисления.

0 голосов
/ 21 мая 2010

Длинный пример кода здесь, и окончательный общий код немного тяжелый ( РЕДАКТИРОВАТЬ добавил дополнительный пример, который устраняет необходимость в угловых скобках за счет некоторой конечной гибкости).

Одна вещь, которую даст вам это решение, - хорошая производительность - не такая хорошая, как у простого оператора switch, но каждый оператор case становится поиском в словаре и вызовом метода, так что все еще довольно хорош. Однако первый вызов получит снижение производительности из-за использования статического универсального шаблона, который отражается при инициализации.

Создайте атрибут и универсальный тип следующим образом:

[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
public class DynamicSwitchAttribute : Attribute
{
 public DynamicSwitchAttribute(Type enumType, params object[] targets)
 { Targets = new HashSet<object>(targets); EnumType = enumType; }
 public Type EnumType { get; private set; }
 public HashSet<object> Targets { get; private set; }
}

//this builds a cache of methods for a given TTarget type, with a 
//signature equal to TAction,
//keyed by values of the type TEnum.  All methods are expected to 
//be instance methods.
//this code can easily be modified to support static methods instead.
//what would be nice here is if we could enforce a generic constraint 
//on TAction : Delegate, but we can't.
public static class DynamicSwitch<TTarget, TEnum, TAction>
{
 //our lookup of actions against enum values.
 //note: no lock is required on this as it is built when the static 
 //class is initialised.

 private static Dictionary<TEnum, TAction> _actions = 
   new Dictionary<TEnum, TAction>();

 private static MethodInfo _tActionMethod;
 private static MethodInfo TActionMethod
 {
  get
  {
   if (_tActionMethod == null)
   {
    //one criticism of this approach might be that validation exceptions
    //will be thrown inside a TypeInitializationException.
    _tActionMethod = typeof(TAction).GetMethod("Invoke", 
      BindingFlags.Instance | BindingFlags.Public);

    if (_tActionMethod == null)
     throw new ArgumentException(/*elided*/);

    //verify that the first parameter type is compatible with our 
    //TTarget type.
    var methodParams = _tActionMethod.GetParameters();
    if (methodParams.Length == 0)
     throw new ArgumentException(/*elided*/);

    //now check that the first parameter is compatible with our type TTarget
    if (!methodParams[0].ParameterType.IsAssignableFrom(typeof(TTarget)))
     throw new ArgumentException(/*elided*/);
   }
   return _tActionMethod;
  }
 }

 static DynamicSwitch()
 {
  //examine the type TTarget to extract all public instance methods 
  //(you can change this to private instance if need be) which have a
  //DynamicSwitchAttribute defined.
  //we then project the attributes and the method into an anonymous type
  var possibleMatchingMethods = 
     from method in typeof(TTarget).
       GetMethods(BindingFlags.Public | BindingFlags.Instance)
     let attributes = method.GetCustomAttributes(
        typeof(DynamicSwitchAttribute), true).
        Cast<DynamicSwitchAttribute>().ToArray()
     where attributes!= null && attributes.Length == 1 
        && attributes[0].EnumType.Equals(typeof(TEnum))
     select new { Method = method, Attribute = attributes[0] };

  //create linq expression parameter expressions for each of the 
  //delegate type's parameters
  //these can be re-used for each of the dynamic methods we generate.
  ParameterExpression[] paramExprs = TActionMethod.GetParameters().
    Select((pinfo, index) =>
    Expression.Parameter(
      pinfo.ParameterType, pinfo.Name ?? string.Format("arg{0}"))
    ).ToArray();
  //pre-build an array of these parameter expressions that only 
  //include the actual parameters
  //for the method, and not the 'this' parameter.
  ParameterExpression[] realParamExprs = paramExprs.Skip(1).ToArray();

  //this has to be generated for each target method.
  MethodCallExpression methodCall = null;

  foreach (var match in possibleMatchingMethods)
  {
   if (!MethodMatchesAction(match.Method))
    continue;

   //right, now we're going to use System.Linq.Expressions to build 
   //a dynamic expression to invoke this method given an instance of TTarget.
   methodCall = 
     Expression.Call(
       Expression.Convert(
         paramExprs[0], typeof(TTarget)
       ),  
       match.Method, realParamExprs);

   TAction dynamicDelegate = Expression.
     Lambda<TAction>(methodCall, paramExprs).Compile();

   //now we have our method, we simply inject it into the dictionary, using 
   //all the unique TEnum values (from the attribute) as the keys
   foreach (var enumValue in match.Attribute.Targets.OfType<TEnum>())
   {
    if (_actions.ContainsKey(enumValue))
     throw new InvalidOperationException(/*elided*/);

    _actions[enumValue] = dynamicDelegate;
   }
  }
 }

 private static bool MethodMatchesAction(MethodInfo method)
 {
  //so we want to check that the target method matches our desired 
  //delegate type (TAction).
  //The way this is done is to fetch the delegate type's Invoke 
  //method (implicitly invoked when you invoke delegate(args)), and 
  //then we check the return type and parameters types of that
  //against the return type and args of the method we've been passed.

  //if the target method's return type is equal to or derived from the 
  //expected delegate's return type, then all is good.

  if (!_tActionMethod.ReturnType.IsAssignableFrom(method.ReturnType))
   return false;

  //now, the parameter lists of the method will not be equal in length, 
  //as our delegate explicitly includes the 'this' parameter, whereas 
  //instance methods do not.

  var methodParams = method.GetParameters();
  var delegateParams = TActionMethod.GetParameters();

  for (int i = 0; i < methodParams.Length; i++)
  {
   if (!methodParams[i].ParameterType.IsAssignableFrom(
        delegateParams[i + 1].ParameterType))
    return false;
  }
  return true;
 }


 public static TAction Resolve(TEnum value)
 {
  TAction result;

  if (!_actions.TryGetValue(value, out result))
   throw new ArgumentException("The value is not mapped");

  return result;
 }
}

Теперь сделайте это в модульном тесте:

[TestMethod]
public void TestMethod1()
{
  Assert.AreEqual(1, 
    DynamicSwitch<UnitTest1, Blah, Func<UnitTest1, int>>.
      Resolve(Blah.BlahBlah)(this));

  Assert.AreEqual(125, 
    DynamicSwitch<UnitTest1, Blah, Func<UnitTest1, int>>.
      Resolve(Blah.Blip)(this));

 Assert.AreEqual(125, 
    DynamicSwitch<UnitTest1, Blah, Func<UnitTest1, int>>.
      Resolve(Blah.Bop)(this));
}

public enum Blah
{
 BlahBlah,
 Bloo,
 Blip,
 Bup,
 Bop
}


[DynamicSwitchAttribute(typeof(Blah), Blah.BlahBlah)]
public int Method()
{
 return 1;
}

[DynamicSwitchAttribute(typeof(Blah), Blah.Blip, Blah.Bop)]
public int Method2()
{
 return 125;
}

Итак, учитывая значение TEnum и предпочитаемый вами тип действия (в вашем коде вы, похоже, просто ничего не возвращаете и не изменяете внутреннее состояние класса), вы просто обращаетесь к классу DynamicSwitch <>, спрашиваете чтобы разрешить целевой метод, а затем вызвать его встроенным (передавая целевой объект, для которого метод будет вызываться в качестве первого параметра).

На самом деле я не ожидаю каких-либо голосов за это - это решение MAD , если честно (оно имеет то преимущество, что его можно применять для любого типа enum и даже дискретных значений типа int / float / double, а также поддержка любого типа делегата) - так что, возможно, это немного кувалдой!

EDIT

Как только у вас будет такой статический универсал, начнётся ад в угловых скобках - поэтому мы хотим попытаться избавиться от них. В большинстве случаев это делается с помощью определения типа параметров метода и т. Д., Но здесь у нас есть проблема , что мы не можем легко вывести подпись делегата без повторения вызова метода , то есть (args) => return.

Однако вам, похоже, требуется метод, который не принимает параметров и возвращает void, поэтому вы можете закрыть этот бегемот-универсал, установив тип делегата в Action, и добавить в API плавный API (если это ваш тип) вещи):

public static class ActionSwitch
{
  public class SwitchOn<TEnum>
  {
    private TEnum Value { get; set; }

    internal SwitchOn(TEnum value)
    {
      Value = value;
    }

    public class Call<TTarget>{
      private TEnum Value { get; set; }
      private TTarget Target { get; set; }

      internal Call(TEnum value, TTarget target)
      {
        Value = value;
        Target = target;
        Invoke();
      }

      internal void Invoke(){
          DynamicSwitch<TTarget, TEnum, Action<TTarget>>.Resolve(Value)(Target);
      }
    }

    public Call<TTarget> On<TTarget>(TTarget target)
    {
      return new Call<TTarget>(Value, target);
    }
  }

  public static SwitchOn<TEnum> Switch<TEnum>(TEnum onValue)
  {
    return new SwitchOn<TEnum>(onValue);
  }
}

Теперь добавьте это в тестовый проект:

[TestMethod]
public void TestMethod2()
{
  //no longer have any angle brackets
  ActionSwitch.Switch(Blah.Bup).On(this);

  Assert.IsTrue(_actionMethod1Called);
}

private bool _actionMethod1Called;

[DynamicSwitch(typeof(Blah), Blah.Bup)]
public void ActionMethod1()
{
  _actionMethod1Called = true;
}

Единственная проблема с этим (не считая сложности решения :)) заключается в том, что вам придется перестраивать этот тип статической оболочки всякий раз, когда вы хотите использовать новый тип целевого делегата для динамического переключения в другом месте. Вы можете сгенерировать универсальную версию на основе делегатов Action <...> и Func <...>, которая включает в себя TArg1, TArg ( n ) и TReturn (если Func <>), но вы бы в конечном итоге написать гораздо больше кода.

Возможно, я превращу это в статью в своем блоге и сделаю все это - если у меня будет время!

0 голосов
/ 21 мая 2010

Иногда сохранение параметров на карте является хорошим решением, вы также можете перенести конфигурацию в файл, не уверенный, применима ли она к вашему приложению.

0 голосов
/ 21 мая 2010

Попробуйте использовать отражение.

  • Украсить параметры перечисления атрибутами, которые содержат связанное значение, и вернуть это значение.
  • Создание статического класса констант и использование отражения для сопоставления enum-опции с константой по имени

надеюсь, что это поможет

...