Asignación en una declaración if

142

Tengo una clase Animaly su subclase Dog. A menudo me encuentro codificando las siguientes líneas:

if (animal is Dog)
{
    Dog dog = animal as Dog;    
    dog.Name;    
    ... 
}

Para la variable Animal animal;.

¿Hay alguna sintaxis que me permita escribir algo como:

if (Dog dog = animal as Dog)
{    
    dog.Name;    
    ... 
}
Miguel
fuente
1
¿Qué significaría eso? ¿Cuál sería la boolcondición?
Kirk Woll
Ninguno que yo sepa. ¿Alguna razón para no mover Nombre a Animal?
AlG
22
Solo una nota, el código como a menudo puede ser el resultado de romper uno de los Principios SOLIDOS . El principio de sustitución de L - Liskov . No digo que esté mal hacer lo que estás haciendo todo el tiempo, pero vale la pena pensarlo.
ckittel
por favor tome nota de lo que está haciendo @ckittel, es probable que no quiere hacer esto
khebbie
1
@Solo no,! null= falseEn C #; C # solo permite bools reales o cosas implícitamente convertibles en bools en ifcondiciones. Ni nulos ni ninguno de los tipos enteros son implícitamente convertibles en bools.
Roman Starkov

Respuestas:

323

La respuesta a continuación se escribió hace años y se actualizó con el tiempo. A partir de C # 7, puede usar la coincidencia de patrones:

if (animal is Dog dog)
{
    // Use dog here
}

Tenga en cuenta que dogtodavía está dentro del alcance después de la ifdeclaración, pero definitivamente no está asignado.


No, no hay Sin embargo, es más idiomático escribir esto:

Dog dog = animal as Dog;
if (dog != null)
{
    // Use dog
}

Dado que "como seguido por if" casi siempre se usa de esta manera, podría tener más sentido que haya un operador que realice ambas partes de una sola vez. Esto no está actualmente en C # 6, pero puede ser parte de C # 7, si se implementa la propuesta de coincidencia de patrones .

El problema es que no puede declarar una variable en la parte de condición de una ifinstrucción 1 . El enfoque más cercano que se me ocurre es este:

// EVIL EVIL EVIL. DO NOT USE.
for (Dog dog = animal as Dog; dog != null; dog = null)
{
    ...
}

Eso es desagradable ... (Acabo de probarlo y funciona. Pero, por favor, no hagas esto. Ah, y puedes declararlo dogusando, varpor supuesto).

Por supuesto, podrías escribir un método de extensión:

public static void AsIf<T>(this object value, Action<T> action) where T : class
{
    T t = value as T;
    if (t != null)
    {
        action(t);
    }
}

Luego llámalo con:

animal.AsIf<Dog>(dog => {
    // Use dog in here
});

Alternativamente, puede combinar los dos:

public static void AsIf<T>(this object value, Action<T> action) where T : class
{
    // EVIL EVIL EVIL
    for (var t = value as T; t != null; t = null)
    {
        action(t);
    }
}

También puede usar un método de extensión sin una expresión lambda de una manera más limpia que el ciclo for:

public static IEnumerable<T> AsOrEmpty(this object value)
{
    T t = value as T;
    if (t != null)
    {
        yield return t;
    }
}

Luego:

foreach (Dog dog in animal.AsOrEmpty<Dog>())
{
    // use dog
}

1 Puede asignar valores en las ifdeclaraciones, aunque rara vez lo hago. Sin embargo, eso no es lo mismo que declarar variables. No es terriblemente inusual que lo haga en un momento whilecuando leo flujos de datos. Por ejemplo:

string line;
while ((line = reader.ReadLine()) != null)
{
    ...
}

En estos días, normalmente prefiero usar un contenedor que me permita usar, foreach (string line in ...)pero veo lo anterior como un patrón bastante idiomático. Por lo general, no es bueno tener efectos secundarios dentro de una afección, pero las alternativas generalmente implican la duplicación de código, y cuando conoce este patrón, es fácil acertar.

Jon Skeet
fuente
76
+1 por dar una respuesta y también suplicar que el OP no la use. Clásico instantáneo.
ckittel
8
@Paul: Si estuviera tratando de venderlo a alguien, no les recomendaría que no lo usen. Solo estoy mostrando lo que es posible .
Jon Skeet
12
@Paul: Creo que esa puede haber sido la motivación detrás EVIL EVIL EVIL, pero no soy positivo.
Adam Robinson
18
Hice un método de extensión similar (con un montón de sobrecargas) hace un tiempo y los llamé AsEither(...), creo que es un poco más claro que AsIf(...), así que puedo escribir myAnimal.AsEither(dog => dog.Woof(), cat => cat.Meeow(), unicorn => unicorn.ShitRainbows()).
herzmeister
97
Ese es el mejor abuso de C # que he visto en mucho tiempo. Claramente eres un genio malvado.
Eric Lippert
48

Si asfalla, vuelve null.

Dog dog = animal as Dog;

if (dog != null)
{
    // do stuff
}
Azul platino
fuente
Primero gracias. En segundo lugar, quiero crear la variable dog en el alcance de la ifdeclaración y no en el alcance externo.
michael
@Michael no puedes hacer eso en una declaración if. El if tiene que tener un resultado bool, no una asignación. Jon Skeet proporciona algunas buenas combinaciones genéricas y lambda que puede considerar también.
Rodney S. Foley
ifpuede tener un resultado bool y una asignación. Dog dog; if ((dog = animal as Dog) != null) { // Use Dog }pero eso todavía introduce la variable en el alcance externo.
Tom Mayfield
12

Usted puede asignar el valor de la variable, siempre y cuando ya existe la variable. También puede definir el alcance de la variable para permitir que el nombre de la variable se use nuevamente más adelante en el mismo método, si eso es un problema.

public void Test()
{
    var animals = new Animal[] { new Dog(), new Duck() };

    foreach (var animal in animals)
    {
        {   // <-- scopes the existence of critter to this block
            Dog critter;
            if (null != (critter = animal as Dog))
            {
                critter.Name = "Scopey";
                // ...
            }
        }

        {
            Duck critter;
            if (null != (critter = animal as Duck))
            {
                critter.Fly();
                // ...
            }
        }
    }
}

asumiendo

public class Animal
{
}

public class Dog : Animal
{
    private string _name;
    public string Name
    {
        get { return _name; }
        set
        {
            _name = value;
            Console.WriteLine("Name is now " + _name);
        }
    }
}

public class Duck : Animal
{
    public void Fly()
    {
        Console.WriteLine("Flying");
    }
}

obtiene salida:

Name is now Scopey
Flying

El patrón de asignación de variables en la prueba también se usa al leer bloques de bytes de las secuencias, por ejemplo:

int bytesRead = 0;
while ((bytesRead = fs.Read(buffer, 0, buffer.Length)) > 0) 
{
    // ...
}

Sin embargo, el patrón de alcance variable utilizado anteriormente no es un patrón de código particularmente común y si lo viera utilizado en todas partes estaría buscando una forma de refactorizarlo.

Artesano
fuente
11

¿Hay alguna sintaxis que me permita escribir algo como:

if (Dog dog = animal as Dog) { ... dog ... }

?

Probablemente habrá en C # 6.0. Esta característica se llama "expresiones de declaración". Ver

https://roslyn.codeplex.com/discussions/565640

para detalles.

La sintaxis propuesta es:

if ((var i = o as int?) != null) {  i  }
else if ((var s = o as string) != null) {  s  }
else if ...

De manera más general, la característica propuesta es que una declaración de variable local puede usarse como una expresión . Esta ifsintaxis es solo una buena consecuencia de la característica más general.

Eric Lippert
fuente
1
De un vistazo, esto parece menos legible que declarar la variable como lo haría hoy. ¿Sabrías por qué esta característica en particular ha logrado pasar la barra de -100 puntos?
asawyer
3
@asawyer: Primero, esta es una característica solicitada con mucha frecuencia. Segundo, otros idiomas tienen esta extensión para "if"; gcc, por ejemplo, permite el equivalente en C ++. En tercer lugar, la característica es más general que solo "if", como señalé. Cuarto, hay una tendencia en C # desde C # 3.0 para hacer más y más cosas que requieren un contexto de declaración en su lugar requieren un contexto de expresión; Esto ayuda con la programación de estilo funcional. Vea las notas de diseño del lenguaje para más detalles.
Eric Lippert
2
@asawyer: ¡De nada! No dude en participar en la discusión en Roslyn.codeplex.com si tiene más comentarios. Además, agregaría: Quinto, la nueva infraestructura de Roslyn reduce los costos marginales para el equipo de implementación de hacer este tipo de pequeñas características experimentales, lo que significa que la magnitud de los puntos "menos 100" disminuye. El equipo está aprovechando esta oportunidad para explorar pequeñas características perfectamente decentes que han sido solicitadas durante mucho tiempo pero que nunca superaron la barrera de los -100 puntos anteriormente.
Eric Lippert
1
Los lectores de estos comentarios que estén confundidos acerca de qué "puntos" de los que estamos hablando deberían leer la publicación del blog del ex diseñador de C # Eric Gunnerson sobre este tema: blogs.msdn.com/b/ericgu/archive/2004/01/12/57985. aspx . Esta es una analogía; no hay "puntos" reales contados.
Eric Lippert
@asawyer: Creo que esta característica realmente brilla en las llamadas a Try*(por ejemplo, TryParse). Esta característica no solo convierte dichas llamadas en una sola expresión (como deberían ser, IMO), sino que también permite una definición más clara de dichas variables. Me entusiasma que el outparámetro de un Trymétodo tenga un alcance condicional; Esto hace que sea más difícil introducir ciertos tipos de errores.
Brian
9

Uno de los métodos de extensión que me encuentro escribiendo y usando a menudo * es

public static TResult IfNotNull<T,TResult>(this T obj, Func<T,TResult> func)
{
    if(obj != null)
    {
        return func(obj);
    }
    return default(TResult);
}

Que podría usarse en esta situación como

string name = (animal as Dog).IfNotNull(x => x.Name);

Y luego namees el nombre del perro (si es un perro), de lo contrario es nulo.

* No tengo idea si esto es efectivo. Nunca ha surgido como un cuello de botella en la elaboración de perfiles.

Greg
fuente
2
+1 para la nota. Si nunca ha surgido como un cuello de botella en la creación de perfiles, es una buena señal de que es lo suficientemente eficiente.
Cody Gray
¿Por qué tomaría el DefaultValue como argumento y dejaría que la persona que llama decidiera qué es lo Z en lugar de volver al valor predeterminado (...)?
Trident D'Gao
5

Ir contra la corriente aquí, pero tal vez lo estás haciendo mal en primer lugar. Verificar el tipo de un objeto casi siempre es un olor a código. ¿No tienen todos los animales, en su ejemplo, un nombre? Luego simplemente llame a Animal.name, sin verificar si es un perro o no.

Alternativamente, invierta el método de modo que llame a un método en Animal que haga algo diferente dependiendo del tipo concreto del Animal. Ver también: polimorfismo.

fwielstra
fuente
4

Declaración más corta

var dog = animal as Dog
if(dog != null) dog.Name ...;
jmogera
fuente
3

Aquí hay un código sucio adicional (aunque no tan sucio como el de Jon :-)) que depende de la modificación de la clase base. Creo que captura la intención mientras tal vez se pierde el punto:

class Animal
{
    public Animal() { Name = "animal";  }
    public List<Animal> IfIs<T>()
    {
        if(this is T)
            return new List<Animal>{this};
        else
            return new List<Animal>();
    }
    public string Name;
}

class Dog : Animal
{
    public Dog() { Name = "dog";  }
    public string Bark { get { return "ruff"; } }
}


class Program
{
    static void Main(string[] args)
    {
        var animal = new Animal();

        foreach(Dog dog in animal.IfIs<Dog>())
        {
            Console.WriteLine(dog.Name);
            Console.WriteLine(dog.Bark);
        }
        Console.ReadLine();
    }
}
James Ashley
fuente
3

Si tiene que hacer múltiples, como-si, uno tras otro (y usar polimorfismo no es una opción), considere usar una construcción SwitchOnType .

Omer Raviv
fuente
3

El problema (con la sintaxis) no está en la asignación, ya que el operador de asignación en C # es una expresión válida. Más bien, es con la declaración deseada, ya que las declaraciones son declaraciones.

Si debo escribir un código como ese, a veces (dependiendo del contexto más amplio) escribiré el código de esta manera:

Dog dog;
if ((dog = animal as Dog) != null) {
    // use dog
}

Hay ventajas con la sintaxis anterior (que está cerca de la sintaxis solicitada) porque:

  1. Usando dog fuera el ifdará lugar a un error de compilación, ya que no se le asigna un valor en otro lugar. (Es decir, no asignar en dogotro lugar).
  2. Este enfoque también se puede ampliar muy bien if/else if/...(solo hay tantos ascomo sea necesario para seleccionar una rama apropiada; este es el gran caso en el que lo escribo en este formulario cuando debo hacerlo).
  3. Evita la duplicación de is/as. (Pero también hecho con Dog dog = ...forma).
  4. No es diferente a "idiomatic while". (Simplemente no se deje llevar: mantenga el condicional en una forma consistente y simple).

Para aislarse verdaderamente dogdel resto del mundo, se puede usar un nuevo bloque:

{
  Dog dog = ...; // or assign in `if` as per above
}
Bite(dog); // oops! can't access dog from above

Feliz codificación


fuente
El punto # 1 que ofrecen es lo primero que me vino a la mente. Declare la variable pero solo asigne en el if. No se puede hacer referencia a la variable desde fuera del if sin un error del compilador, ¡perfecto!
Ian Yates
1

puedes usar algo así

// Declarar variable bool temp = false;

 if (previousRows.Count > 0 || (temp= GetAnyThing()))
                                    {
                                    }
NobDev
fuente
0

Otra solución MALA con métodos de extensión :)

public class Tester
{
    public static void Test()
    {
        Animal a = new Animal();

        //nothing is printed
        foreach (Dog d in a.Each<Dog>())
        {
            Console.WriteLine(d.Name);
        }

        Dog dd = new Dog();

        //dog ID is printed
        foreach (Dog dog in dd.Each<Dog>())
        {
            Console.WriteLine(dog.ID);
        }
    }
}

public class Animal
{
    public Animal()
    {
        Console.WriteLine("Animal constructued:" + this.ID);
    }

    private string _id { get; set; }

    public string ID { get { return _id ?? (_id = Guid.NewGuid().ToString());} }

    public bool IsAlive { get; set; }
}

public class Dog : Animal 
{
    public Dog() : base() { }

    public string Name { get; set; }
}

public static class ObjectExtensions
{
    public static IEnumerable<T> Each<T>(this object Source)
        where T : class
    {
        T t = Source as T;

        if (t == null)
            yield break;

        yield return t;
    }
}

Personalmente prefiero la forma limpia:

Dog dog = animal as Dog;

if (dog != null)
{
    // do stuff
}
Stefan Michev
fuente
0

Una declaración if no lo permitirá, pero un bucle for sí.

p.ej

for (Dog dog = animal as Dog; dog != null; dog = null)
{
    dog.Name;    
    ... 
}

En caso de que la forma en que funciona no sea obvia de inmediato, aquí hay una explicación paso a paso del proceso:

  • El perro variable se crea como tipo perro y se le asigna el animal variable que se lanza al perro.
  • Si la asignación falla, dog es nulo, lo que impide que se ejecute el contenido del bucle for, porque se rompe inmediatamente.
  • Si la asignación tiene éxito, el bucle for se ejecuta a través de la
    iteración.
  • Al final de la iteración, a la variable dog se le asigna un valor de nulo, que sale del ciclo for.
WonderWorker
fuente
0
using(Dog dog = animal as Dog)
{
    if(dog != null)
    {
        dog.Name;    
        ... 

    }

}
WonderWorker
fuente
0

IDK si esto ayuda a alguien pero siempre puede intentar usar un TryParse para asignar su variable. Aquí hay un ejemplo:

if (int.TryParse(Add(Value1, Value2).ToString(), out total))
        {
            Console.WriteLine("I was able to parse your value to: " + total);
        } else
        {
            Console.WriteLine("Couldn't Parse Value");
        }


        Console.ReadLine();
    }

    static int Add(int value1, int value2)
    {
        return value1 + value2;
    }

La variable total se declararía antes de su declaración if.

Alejandro Garcia
fuente
0

Acabo de insertar la instrucción if para crear una línea de código que se parezca a lo que le interesa. Simplemente ayuda a comprimir el código y descubrí que es más legible, especialmente cuando se anidan asignaciones:

var dog = animal as Dog; if (dog != null)
{
    Console.WriteLine("Parent Dog Name = " + dog.name);

    var purebred = dog.Puppy as Purebred; if (purebred != null)
    {
         Console.WriteLine("Purebred Puppy Name = " + purebred.Name);
    }

    var mutt = dog.Puppy as Mongrel; if (mutt != null)
    {
         Console.WriteLine("Mongrel Puppy Name = " + mutt.Name);
    }
 }
usuario1689175
fuente
0

Sé que llego súper tonto tarde a la fiesta, pero pensé que publicaría mi propia solución a este dilema ya que aún no lo he visto aquí (ni en ningún otro lado).

/// <summary>
/// IAble exists solely to give ALL other Interfaces that inherit IAble the TryAs() extension method
/// </summary>
public interface IAble { }

public static class IAbleExtension
{
    /// <summary>
    /// Attempt to cast as T returning true and out-ing the cast if successful, otherwise returning false and out-ing null
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="able"></param>
    /// <param name="result"></param>
    /// <returns></returns>
    public static bool TryAs<T>(this IAble able, out T result) where T : class
    {
        if (able is T)
        {
            result = able as T;
            return true;
        }
        else
        {
            result = null;
            return false;
        }
    }

    /// <summary>
    /// Attempt to cast as T returning true and out-ing the cast if successful, otherwise returning false and out-ing null
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="obj"></param>
    /// <param name="result"></param>
    /// <returns></returns>
    public static bool TryAs<T>(this UnityEngine.Object obj, out T result) where T : class
    {
        if (obj is T)
        {
            result = obj as T;
            return true;
        }
        else
        {
            result = null;
            return false;
        }
    }
}

Con esto, puedes hacer cosas como:

if (animal.TryAs(out Dog dog))
{
    //Do Dog stuff here because animal is a Dog
}
else
{
    //Cast failed! animal is not a dog
}

NOTA IMPORTANTE: Si desea usar TryAs () usando una interfaz, DEBE tener esa interfaz heredando IAble.

¡Disfrutar! 🙂

Darian Lehmann-Plantenberg
fuente