Лучшая альтернатива случаю
В настоящее время у меня есть оператор 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
}
Ответы
Ответ 1
ИЗМЕНИТЬ
Я снова подумал об этом, посмотрел вокруг в связанных вопросах в 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
дважды нажмите вкладку, и она автоматически генерирует все возможности для вас.
Затем добавьте случай default
к концу, который выдает исключение, таким образом, при тестировании вашего приложения вы заметите, что есть необработанный случай, мгновенно.
Я имею в виду что-то вроде этого:
switch (something)
{
...
case YourEnum.SomeValue:
...
break;
default:
throw new InvalidOperationException("Default case reached.");
}
Ответ 2
Ну, там бросается в случае default
... Там нет конструкции редактирования/компиляции, кроме этого.
Однако стратегия, Посетитель и другие связанные с ними шаблоны могут быть подходящими, если вы решите сделать это во время выполнения.
Примерный код поможет получить лучший ответ.
EDIT: Спасибо за образец. Я все еще думаю, что это требует немного флеширования, поскольку вы не освещаете, есть ли некоторые параметры, которые применимы только к некоторым case
и т.д.
Действие часто используется как псевдоним для шаблона Command и тот факт, что ваш Enum называется LogAction
означает, что каждое значение несет с собой поведение - подразумевается (вы вставляете соответствующий код в case
) или явно (в конкретном классе иерархии команд).
Таким образом, мне кажется, что использование шаблона Command является подходящим (хотя ваш пример не доказывает его), т.е. иметь класс (потенциально иерархию с использованием перегрузок конструктора или любой другой [набор] механизмов factory) который сохраняет состояние, связанное с запросом, вместе с конкретным поведением. Затем вместо того, чтобы передавать значение Enum, создайте соответствующий экземпляр LogCommand
для регистратора, который просто вызывает его (потенциально передавая приемник Log Sink ', который может войти в команду). В противном случае вы будете вызывать случайные подмножества параметров в разных местах.
Сообщения, связанные с SEEALSO:
Ответ 3
Одним из возможных решений является использование 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
}
}
Этот метод позволяет динамически заменять обработчики. Вы также можете использовать Список как часть значения словаря и иметь несколько обработчиков для каждого перечисления.
Ответ 4
Попробуйте использовать отражение.
- Украсить параметры перечисления атрибутами, которые содержат связанное значение и вернуть это значение.
- Создать статический класс констант и использовать отражение для отображения enum-option для константы по имени
надеюсь, что это поможет
Ответ 5
Некоторое время хранение опций на карте является хорошим решением, вы также можете экпортировать конфигурацию в файл, не уверен, применимо ли оно к вашему приложению.
Ответ 6
Длинный пример кода, а окончательный общий код немного тяжелый (EDIT добавили дополнительный пример, который устраняет необходимость в угловых скобках за счет некоторой окончательной гибкости).
Одна вещь, которую это решение даст вам, - хорошая производительность - не так хорошо, как простой оператор 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 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 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 return type is equal to or derived from the
//expected delegate 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;
}
}
Теперь сделайте это в Unit Test:
[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 < > , спросите он разрешает целевой метод, а затем вызывает его inline (передача целевого объекта, на который метод будет вызываться в качестве первого параметра).
Я не ожидаю никаких голосов за это - это честное решение MAD (у него есть то преимущество, что он может применяться для любого типа перечисления, и даже сдержанные значения тип int/float/double, а также поддержка любого типа делегата) - возможно, это немного кувалдой!
ИЗМЕНИТЬ
Как только у вас есть статический родословный подобный, появляется угловой скот, и мы хотим попытаться избавиться от них. В большинстве случаев это делается путем вывода типа в параметрах метода и т.д., Но мы имеем здесь проблему, что мы не можем легко заключить подпись делегата, не повторяя вызов метода i.e. (args) => return
.
Однако вам кажется, что требуется метод, который не принимает никаких параметров и возвращает void, поэтому вы можете закрыть этот общий генезис, установив тип делегирования в Action, и также выбросите API-интерфейс Fluid в микс (если это ваш вид вещи):
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 < > ) - но вы в конечном итоге напишите намного больше код.
Возможно, я превращу это в статью о своем блоге и сделаю все это - если я найду время!