net - remarks c#
¿Hay una alternativa mejor que esta para ''activar el tipo''? (24)
Como sugiere Pablo, el enfoque de interfaz es casi siempre lo correcto para manejar esto. Para utilizar realmente el interruptor, otra alternativa es tener una enumeración personalizada que indique su tipo en sus clases.
enum ObjectType { A, B, Default }
interface IIdentifiable
{
ObjectType Type { get; };
}
class A : IIdentifiable
{
public ObjectType Type { get { return ObjectType.A; } }
}
class B : IIdentifiable
{
public ObjectType Type { get { return ObjectType.B; } }
}
void Foo(IIdentifiable o)
{
switch (o.Type)
{
case ObjectType.A:
case ObjectType.B:
//......
}
}
Esto también está implementado en BCL. Un ejemplo es MemberInfo.MemberTypes , otro es GetTypeCode
para tipos primitivos, como:
void Foo(object o)
{
switch (Type.GetTypeCode(o.GetType())) // for IConvertible, just o.GetTypeCode()
{
case TypeCode.Int16:
case TypeCode.Int32:
//etc ......
}
}
Al ver que C # no puede activar un Tipo (el cual, según tengo entendido, no se agregó como un caso especial porque es una relación significa que podría aplicarse más de un caso distinto), ¿existe una mejor manera de simular el cambio de tipo que este ?
void Foo(object o)
{
if (o is A)
{
((A)o).Hop();
}
else if (o is B)
{
((B)o).Skip();
}
else
{
throw new ArgumentException("Unexpected type: " + o.GetType());
}
}
Con la respuesta de JaredPar en la parte de atrás de mi cabeza, escribí una variante de su clase TypeSwitch
que usa la inferencia de tipos para una sintaxis más agradable:
class A { string Name { get; } }
class B : A { string LongName { get; } }
class C : A { string FullName { get; } }
class X { public string ToString(IFormatProvider provider); }
class Y { public string GetIdentifier(); }
public string GetName(object value)
{
string name = null;
TypeSwitch.On(value)
.Case((C x) => name = x.FullName)
.Case((B x) => name = x.LongName)
.Case((A x) => name = x.Name)
.Case((X x) => name = x.ToString(CultureInfo.CurrentCulture))
.Case((Y x) => name = x.GetIdentifier())
.Default((x) => name = x.ToString());
return name;
}
Tenga en cuenta que el orden de los métodos Case()
es importante.
Obtenga el código completo y comentado para mi clase TypeSwitch
. Esta es una versión abreviada de trabajo:
public static class TypeSwitch
{
public static Switch<TSource> On<TSource>(TSource value)
{
return new Switch<TSource>(value);
}
public sealed class Switch<TSource>
{
private readonly TSource value;
private bool handled = false;
internal Switch(TSource value)
{
this.value = value;
}
public Switch<TSource> Case<TTarget>(Action<TTarget> action)
where TTarget : TSource
{
if (!this.handled && this.value is TTarget)
{
action((TTarget) this.value);
this.handled = true;
}
return this;
}
public void Default(Action<TSource> action)
{
if (!this.handled)
action(this.value);
}
}
}
Cree una interfaz que pueda ser compatible, luego haga que sus clases A y B implementen un método común, que a su vez llama al método correspondiente que desea:
interface IFooable
{
public void Foo();
}
class A : IFooable
{
//other methods ...
public void Foo()
{
this.Hop();
}
}
class B : IFooable
{
//other methods ...
public void Foo()
{
this.Skip();
}
}
class ProcessingClass
{
public void Foo(object o)
{
if (o == null)
throw new NullRefferenceException("Null reference", "o");
IFooable f = o as IFooable;
if (f != null)
{
f.Foo();
}
else
{
throw new ArgumentException("Unexpected type: " + o.GetType());
}
}
}
Tenga en cuenta que es mejor usar "como" en lugar de verificar primero con "es" y luego lanzar, ya que de esa manera usted hace 2 lanzamientos (caro).
Cree una superclase (S) y haga que A y B hereden de ella. Luego declare un método abstracto en S que cada subclase necesita implementar.
Al hacer esto, el método "foo" también puede cambiar su firma a Foo (S o), hacer que su tipo sea seguro y no es necesario lanzar esa fea excepción.
Dada la herencia facilita que un objeto sea reconocido como más de un tipo, creo que un cambio podría llevar a una mala ambigüedad. Por ejemplo:
Caso 1
{
string s = "a";
if (s is string) Print("Foo");
else if (s is object) Print("Bar");
}
Caso 2
{
string s = "a";
if (s is object) Print("Foo");
else if (s is string) Print("Bar");
}
Porque s es una cadena y un objeto. Creo que cuando escribes un switch(foo)
esperas que foo coincida con una y solo una de las afirmaciones del case
. Con un cambio en los tipos, el orden en el que escribe las declaraciones de su caso podría cambiar el resultado de toda la declaración del cambio. Creo que eso estaría mal.
Podría pensar en una verificación del compilador en los tipos de una declaración "typeswitch", verificando que los tipos enumerados no se heredan unos de otros. Eso no existe sin embargo.
foo is T
no es lo mismo que foo.GetType() == typeof(T)
!!
En estos casos, generalmente termino con una lista de predicados y acciones. Algo a lo largo de estas líneas:
class Mine {
static List<Func<object, bool>> predicates;
static List<Action<object>> actions;
static Mine() {
AddAction<A>(o => o.Hop());
AddAction<B>(o => o.Skip());
}
static void AddAction<T>(Action<T> action) {
predicates.Add(o => o is T);
actions.Add(o => action((T)o);
}
static void RunAction(object o) {
for (int i=0; o < predicates.Count; i++) {
if (predicates[i](o)) {
actions[i](o);
break;
}
}
}
void Foo(object o) {
RunAction(o);
}
}
Estás buscando Discriminated Unions
que son una característica del lenguaje de F #, pero puedes lograr un efecto similar utilizando una biblioteca que hice, llamada OneOf
https://github.com/mcintyre321/OneOf
La principal ventaja sobre el switch
(y if
y las exceptions as control flow
) es que es seguro en tiempo de compilación: no hay un controlador predeterminado o falla.
void Foo(OneOf<A, B> o)
{
o.Switch(
a => a.Hop(),
b => b.Skip()
);
}
Si agrega un tercer elemento a o, obtendrá un error del compilador ya que tiene que agregar una función de manejador dentro de la llamada del switch.
También puede hacer un .Match
que devuelve un valor, en lugar de ejecutar una declaración:
double Area(OneOf<Square, Circle> o)
{
return o.Match(
square => square.Length * square.Length,
circle => Math.PI * circle.Radius * circle.Radius
);
}
Esta es una respuesta alternativa que combina las contribuciones de las respuestas de JaredPar y VirtLink, con las siguientes restricciones:
- La construcción del interruptor se comporta como una función y recibe funciones como parámetros para los casos.
- Asegura que esté construido correctamente y que siempre exista una función predeterminada .
- Vuelve después de la primera coincidencia (verdadero para la respuesta de JaredPar, no verdadero para VirtLink uno).
Uso:
var result =
TSwitch<string>
.On(val)
.Case((string x) => "is a string")
.Case((long x) => "is a long")
.Default(_ => "what is it?");
Código:
public class TSwitch<TResult>
{
class CaseInfo<T>
{
public Type Target { get; set; }
public Func<object, T> Func { get; set; }
}
private object _source;
private List<CaseInfo<TResult>> _cases;
public static TSwitch<TResult> On(object source)
{
return new TSwitch<TResult> {
_source = source,
_cases = new List<CaseInfo<TResult>>()
};
}
public TResult Default(Func<object, TResult> defaultFunc)
{
var srcType = _source.GetType();
foreach (var entry in _cases)
if (entry.Target.IsAssignableFrom(srcType))
return entry.Func(_source);
return defaultFunc(_source);
}
public TSwitch<TResult> Case<TSource>(Func<TSource, TResult> func)
{
_cases.Add(new CaseInfo<TResult>
{
Func = x => func((TSource)x),
Target = typeof(TSource)
});
return this;
}
}
Estoy de acuerdo con Jon sobre tener un hash de acciones al nombre de la clase. Si mantiene su patrón, podría considerar usar la construcción "como" en su lugar:
A a = o as A;
if (a != null) {
a.Hop();
return;
}
B b = o as B;
if (b != null) {
b.Skip();
return;
}
throw new ArgumentException("...");
La diferencia es que cuando usas el patrón si (foo es Bar) {((Bar) foo) .Action (); } estás haciendo el casting de tipo dos veces. Ahora tal vez el compilador se optimizará y solo hará ese trabajo una vez, pero no contaría con eso.
La activación de los tipos definitivamente carece de C # ( ACTUALIZACIÓN: en C # 7 / VS 2017 se admite la activación de los tipos; consulte la respuesta de Zachary Yates a continuación ). Para hacer esto sin una gran instrucción if / else if / else, deberá trabajar con una estructura diferente. Hace un tiempo escribí una entrada de blog que detallaba cómo construir una estructura TypeSwitch.
http://blogs.msdn.com/jaredpar/archive/2008/05/16/switching-on-types.aspx
Versión corta: TypeSwitch está diseñado para evitar la conversión redundante y otorgar una sintaxis similar a una declaración de conmutador / caso normal. Por ejemplo, aquí está TypeSwitch en acción en un evento de formulario estándar de Windows
TypeSwitch.Do(
sender,
TypeSwitch.Case<Button>(() => textBox1.Text = "Hit a Button"),
TypeSwitch.Case<CheckBox>(x => textBox1.Text = "Checkbox is " + x.Checked),
TypeSwitch.Default(() => textBox1.Text = "Not sure what is hovered over"));
El código para TypeSwitch es en realidad bastante pequeño y se puede poner fácilmente en su proyecto.
static class TypeSwitch {
public class CaseInfo {
public bool IsDefault { get; set; }
public Type Target { get; set; }
public Action<object> Action { get; set; }
}
public static void Do(object source, params CaseInfo[] cases) {
var type = source.GetType();
foreach (var entry in cases) {
if (entry.IsDefault || entry.Target.IsAssignableFrom(type)) {
entry.Action(source);
break;
}
}
}
public static CaseInfo Case<T>(Action action) {
return new CaseInfo() {
Action = x => action(),
Target = typeof(T)
};
}
public static CaseInfo Case<T>(Action<T> action) {
return new CaseInfo() {
Action = (x) => action((T)x),
Target = typeof(T)
};
}
public static CaseInfo Default(Action action) {
return new CaseInfo() {
Action = x => action(),
IsDefault = true
};
}
}
Me gustó el uso de la escritura implícita por parte de Virtlink para hacer el cambio mucho más legible, pero no me gustó que no fuera posible un inicio temprano, y que estamos haciendo asignaciones. Vamos a subir el perf un poco.
public static class TypeSwitch
{
public static void On<TV, T1>(TV value, Action<T1> action1)
where T1 : TV
{
if (value is T1) action1((T1)value);
}
public static void On<TV, T1, T2>(TV value, Action<T1> action1, Action<T2> action2)
where T1 : TV where T2 : TV
{
if (value is T1) action1((T1)value);
else if (value is T2) action2((T2)value);
}
public static void On<TV, T1, T2, T3>(TV value, Action<T1> action1, Action<T2> action2, Action<T3> action3)
where T1 : TV where T2 : TV where T3 : TV
{
if (value is T1) action1((T1)value);
else if (value is T2) action2((T2)value);
else if (value is T3) action3((T3)value);
}
// ... etc.
}
Bueno, eso hace que me duelen los dedos. Hagámoslo en T4:
<#@ template debug="false" hostSpecific="true" language="C#" #>
<#@ output extension=".cs" #>
<#@ Assembly Name="System.Core.dll" #>
<#@ import namespace="System.Linq" #>
<#@ import namespace="System.IO" #>
<#
string GenWarning = "// THIS FILE IS GENERATED FROM " + Path.GetFileName(Host.TemplateFile) + " - ANY HAND EDITS WILL BE LOST!";
const int MaxCases = 15;
#>
<#=GenWarning#>
using System;
public static class TypeSwitch
{
<# for(int icase = 1; icase <= MaxCases; ++icase) {
var types = string.Join(", ", Enumerable.Range(1, icase).Select(i => "T" + i));
var actions = string.Join(", ", Enumerable.Range(1, icase).Select(i => string.Format("Action<T{0}> action{0}", i)));
var wheres = string.Join(" ", Enumerable.Range(1, icase).Select(i => string.Format("where T{0} : TV", i)));
#>
<#=GenWarning#>
public static void On<TV, <#=types#>>(TV value, <#=actions#>)
<#=wheres#>
{
if (value is T1) action1((T1)value);
<# for(int i = 2; i <= icase; ++i) { #>
else if (value is T<#=i#>) action<#=i#>((T<#=i#>)value);
<#}#>
}
<#}#>
<#=GenWarning#>
}
Ajustando un poco el ejemplo de Virtlink:
TypeSwitch.On(operand,
(C x) => name = x.FullName,
(B x) => name = x.LongName,
(A x) => name = x.Name,
(X x) => name = x.ToString(CultureInfo.CurrentCulture),
(Y x) => name = x.GetIdentifier(),
(object x) => name = x.ToString());
Legible y rápido. Ahora, como todo el mundo sigue señalando en sus respuestas, y dada la naturaleza de esta pregunta, el orden es importante en la coincidencia de tipos. Por lo tanto:
- Ponga primero los tipos de hojas, luego los tipos de base.
- Para los tipos de pares, ponga primero las coincidencias más probables para maximizar el rendimiento.
- Esto implica que no hay necesidad de un caso predeterminado especial. En su lugar, simplemente use el tipo de base más en la lambda, y póngalo en último lugar.
Me gustaría crear una interfaz con cualquier nombre y nombre de método que tenga sentido para su conmutador, llamémoslos respectivamente: IDoable que le dice a implementar void Do ().
public interface IDoable
{
void Do();
}
public class A : IDoable
{
public void Hop()
{
// ...
}
public void Do()
{
Hop();
}
}
public class B : IDoable
{
public void Skip()
{
// ...
}
public void Do()
{
Skip();
}
}
y cambiar el método de la siguiente manera:
void Foo<T>(T obj)
where T : IDoable
{
// ...
obj.Do();
// ...
}
Al menos con eso, estás a salvo en el momento de la compilación y sospecho que en cuanto al rendimiento es mejor que comprobar el tipo en tiempo de ejecución.
Otra forma sería definir un IThing de interfaz y luego implementarlo en ambas clases aquí está el snipet:
public interface IThing
{
void Move();
}
public class ThingA : IThing
{
public void Move()
{
Hop();
}
public void Hop(){
//Implementation of Hop
}
}
public class ThingA : IThing
{
public void Move()
{
Skip();
}
public void Skip(){
//Implementation of Skip
}
}
public class Foo
{
static void Main(String[] args)
{
}
private void Foo(IThing a)
{
a.Move();
}
}
Para los tipos incorporados, puede utilizar la enumeración TypeCode. Tenga en cuenta que GetType () es un poco lento, pero probablemente no sea relevante en la mayoría de las situaciones.
switch (Type.GetTypeCode(someObject.GetType()))
{
case TypeCode.Boolean:
break;
case TypeCode.Byte:
break;
case TypeCode.Char:
break;
}
Para los tipos personalizados, puede crear su propia enumeración y una interfaz o una clase base con propiedad o método abstracto ...
Clase abstracta de implementación de propiedad.
public enum FooTypes { FooFighter, AbbreviatedFool, Fubar, Fugu };
public abstract class Foo
{
public abstract FooTypes FooType { get; }
}
public class FooFighter : Foo
{
public override FooTypes FooType { get { return FooTypes.FooFighter; } }
}
Clase abstracta de implementación del método.
public enum FooTypes { FooFighter, AbbreviatedFool, Fubar, Fugu };
public abstract class Foo
{
public abstract FooTypes GetFooType();
}
public class FooFighter : Foo
{
public override FooTypes GetFooType() { return FooTypes.FooFighter; }
}
Implementación de interfaz de propiedad.
public enum FooTypes { FooFighter, AbbreviatedFool, Fubar, Fugu };
public interface IFooType
{
FooTypes FooType { get; }
}
public class FooFighter : IFooType
{
public FooTypes FooType { get { return FooTypes.FooFighter; } }
}
Implementación de la interfaz del método.
public enum FooTypes { FooFighter, AbbreviatedFool, Fubar, Fugu };
public interface IFooType
{
FooTypes GetFooType();
}
public class FooFighter : IFooType
{
public FooTypes GetFooType() { return FooTypes.FooFighter; }
}
Uno de mis compañeros de trabajo me comentó esto también: esto tiene la ventaja de que puede usarlo para cualquier tipo de objeto, no solo para los que defina. Tiene la desventaja de ser un poco más grande y más lento.
Primero define una clase estática como esta:
public static class TypeEnumerator
{
public class TypeEnumeratorException : Exception
{
public Type unknownType { get; private set; }
public TypeEnumeratorException(Type unknownType) : base()
{
this.unknownType = unknownType;
}
}
public enum TypeEnumeratorTypes { _int, _string, _Foo, _TcpClient, };
private static Dictionary<Type, TypeEnumeratorTypes> typeDict;
static TypeEnumerator()
{
typeDict = new Dictionary<Type, TypeEnumeratorTypes>();
typeDict[typeof(int)] = TypeEnumeratorTypes._int;
typeDict[typeof(string)] = TypeEnumeratorTypes._string;
typeDict[typeof(Foo)] = TypeEnumeratorTypes._Foo;
typeDict[typeof(System.Net.Sockets.TcpClient)] = TypeEnumeratorTypes._TcpClient;
}
/// <summary>
/// Throws NullReferenceException and TypeEnumeratorException</summary>
/// <exception cref="System.NullReferenceException">NullReferenceException</exception>
/// <exception cref="MyProject.TypeEnumerator.TypeEnumeratorException">TypeEnumeratorException</exception>
public static TypeEnumeratorTypes EnumerateType(object theObject)
{
try
{
return typeDict[theObject.GetType()];
}
catch (KeyNotFoundException)
{
throw new TypeEnumeratorException(theObject.GetType());
}
}
}
Y luego puedes usarlo así:
switch (TypeEnumerator.EnumerateType(someObject))
{
case TypeEnumerator.TypeEnumeratorTypes._int:
break;
case TypeEnumerator.TypeEnumeratorTypes._string:
break;
}
Realmente debería estar sobrecargando su método, no tratando de hacer la desambiguación usted mismo. Hasta ahora, la mayoría de las respuestas no tienen en cuenta las subclases futuras, lo que puede llevar a problemas de mantenimiento realmente terribles más adelante.
Sí, gracias a C # 7, esto se puede lograr, así es como se hace (usando un patrón de expresión ):
switch(o)
{
case A a:
a.Hop();
break;
case B b:
b.Skip();
break;
case C _:
return new ArgumentException("Type C will be supported in the next version");
default:
return new ArgumentException("Unexpected type: " + o.GetType());
}
Sí, solo use la "combinación de patrones" de C # 7 para nombrar en clase o estructura:
IObject concrete1 = new ObjectImplementation1();
IObject concrete2 = new ObjectImplementation2();
switch (concrete1)
{
case ObjectImplementation1 c1: return "type 1";
case ObjectImplementation2 c2: return "type 2";
}
Según la especificación de C # 7.0, puede declarar una variable local con ámbito en el case
de un switch
:
object a = "Hello world";
switch (a)
{
case string _:
// The variable ''a'' is a string!
break;
case int _:
// The variable ''a'' is an int!
break;
case Foo _:
// The variable ''a'' is of type Foo!
break;
}
Por casualidad, ¿estás preguntando por qué la variable se declara como string _
? ¿Por qué el guión bajo?
Bueno, otra característica introducida con C # 7.0 es que puede nombrar como una variable a la que nunca hace referencia. Por lo tanto, no puede hacer referencia a la variable _
. Es bueno en muchos escenarios, tal como lo solicitó el OP, ya que él solo quiere verificar el tipo y no obtener también una referencia emitida. De lo contrario, puede cambiar el nombre de esa variable y usarla como referencia como desee.
Esta es la mejor manera de hacer tal cosa, ya que implica solo operaciones de conversión y de empuje en la pila, que son las operaciones más rápidas que un intérprete puede ejecutar justo después de las operaciones bitwise y las condiciones boolean
.
Comparando esto con un Dictionary<K, V>
, aquí hay mucho menos uso de memoria: almacenar un diccionario requiere más espacio en la RAM y algunos cálculos más por parte de la CPU para crear dos matrices (una para las claves y la otra para los valores) y la recopilación de hash Códigos de las claves para poner valores a sus respectivas claves.
Entonces, por lo que sé, no creo que pueda existir una forma más rápida incluso si no quiere usar solo un bloque if
- then
- else
con el operador is
siguiente manera:
object a = "Hello world";
if (a is string)
{
// The variable ''a'' is a string!
} else if (a is int)
{
// The variable ''a'' is an int!
} // etc.
Si estuviera utilizando C # 4, podría hacer uso de la nueva funcionalidad dinámica para lograr una alternativa interesante. No digo que esto sea mejor, de hecho parece muy probable que sea más lento, pero tiene cierta elegancia.
class Thing
{
void Foo(A a)
{
a.Hop();
}
void Foo(B b)
{
b.Skip();
}
}
Y el uso:
object aOrB = Get_AOrB();
Thing t = GetThing();
((dynamic)t).Foo(aorB);
La razón por la que funciona es que la invocación de un método dinámico C # 4 tiene sus sobrecargas resueltas en tiempo de ejecución en lugar de compilar. Escribí un poco más sobre esta idea bastante recientemente . Una vez más, me gustaría reiterar que esto probablemente se comporte peor que todas las otras sugerencias, lo ofrezco simplemente como una curiosidad.
Una opción es tener un diccionario de Type
a Action
(o algún otro delegado). Busque la acción según el tipo y luego ejecútela. He usado esto para las fábricas antes de ahora.
Yo tampoco
- usar la sobrecarga de métodos (como x0n ), o
- usar subclases (como ), o
- Aplicar el patrón de visitante .
Con C # 7 , que se distribuyó con Visual Studio 2017 (versión 15. *), puede utilizar los tipos en declaraciones de case
(coincidencia de patrones):
switch(shape)
{
case Circle c:
WriteLine($"circle with radius {c.Radius}");
break;
case Rectangle s when (s.Length == s.Height):
WriteLine($"{s.Length} x {s.Height} square");
break;
case Rectangle r:
WriteLine($"{r.Length} x {r.Height} rectangle");
break;
default:
WriteLine("<unknown shape>");
break;
case null:
throw new ArgumentNullException(nameof(shape));
}
Con C # 6, puede usar una instrucción switch con el operador nameof () (gracias @Joey Adams):
switch(o.GetType().Name) {
case nameof(AType):
break;
case nameof(BType):
break;
}
Con C # 5 y versiones anteriores, podría usar una instrucción de cambio, pero tendrá que usar una cadena mágica que contenga el nombre del tipo ... que no es particularmente fácil de refactorizar (gracias @nukefusion)
switch(o.GetType().Name) {
case "AType":
break;
}