我有一个类 Animal
及其子类 Dog
。我经常发现自己在编写以下代码行:
if (animal is Dog)
{
Dog dog = animal as Dog;
dog.Name;
...
}
对于变量 Animal animal;
。
是否有一些语法可以让我编写如下内容:
if (Dog dog = animal as Dog)
{
dog.Name;
...
}
null
被强制转换为 false
。因此,如果赋值的结果为 null,则语句计算为 null
,然后将其强制转换为 false
。这就是为什么在某些语言中,只有当该变量不为空时,您才能使用 if(var) {...}
来执行代码。通常,在 if
语句中赋值给变量是很糟糕的,因为它看起来像一个常见错误(输入 =
而不是 ==
)。过去可以通过在 if
块中使用赋值来获得一些(小的)性能优势,但是许多现代编译器会为您做这样的优化。
null
!= false
; C# 只允许实际的布尔值或在 if
条件下隐式转换为布尔值的东西。空值和任何整数类型都不能隐式转换为布尔值。
下面的答案是几年前写的,并随着时间的推移而更新。从 C# 7 开始,您可以使用模式匹配:
if (animal is Dog dog)
{
// Use dog here
}
请注意,dog
仍在 if
语句之后的范围内,但未明确分配。
不,没有。写这个更惯用:
Dog dog = animal as Dog;
if (dog != null)
{
// Use dog
}
鉴于“as follow by if”几乎总是以这种方式使用,因此有一个操作符一次性执行这两个部分可能更有意义。这目前不在 C# 6 中,但如果实现了 pattern matching proposal,它可能是 C# 7 的一部分。
问题是您不能在 if
语句1 的条件部分中声明变量。我能想到的最接近的方法是:
// EVIL EVIL EVIL. DO NOT USE.
for (Dog dog = animal as Dog; dog != null; dog = null)
{
...
}
这只是讨厌...(我刚刚尝试过,它确实有效。但是请不要这样做。哦,您可以使用 var
声明 dog
课程。)
当然你可以写一个扩展方法:
public static void AsIf<T>(this object value, Action<T> action) where T : class
{
T t = value as T;
if (t != null)
{
action(t);
}
}
然后调用它:
animal.AsIf<Dog>(dog => {
// Use dog in here
});
或者,您可以将两者结合起来:
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);
}
}
您还可以以比 for 循环更简洁的方式使用没有 lambda 表达式的扩展方法:
public static IEnumerable<T> AsOrEmpty(this object value)
{
T t = value as T;
if (t != null)
{
yield return t;
}
}
然后:
foreach (Dog dog in animal.AsOrEmpty<Dog>())
{
// use dog
}
1 您可以在 if
语句中分配值,尽管我很少这样做。但是,这与声明变量不同。虽然在读取数据流时,我在 while
中执行此操作并不非常不寻常。例如:
string line;
while ((line = reader.ReadLine()) != null)
{
...
}
这些天来,我通常更喜欢使用允许我使用 foreach (string line in ...)
的包装器,但我认为上面是一个非常惯用的模式。 通常在条件中产生副作用并不好,但替代方案通常涉及代码重复,当您知道这种模式时,很容易得到正确的。
如果 as
失败,则返回 null
。
Dog dog = animal as Dog;
if (dog != null)
{
// do stuff
}
if
语句的范围内而不是在外部范围内创建 dog 变量。
if
可以有一个布尔结果和一个赋值。 Dog dog; if ((dog = animal as Dog) != null) { // Use Dog }
但这仍然会在外部范围内引入变量。
只要变量已经存在,您就可以将值分配给变量。如果这是一个问题,您还可以限定变量以允许稍后在同一方法中再次使用该变量名称。
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();
// ...
}
}
}
}
假设
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");
}
}
得到输出:
Name is now Scopey
Flying
当从流中读取字节块时,也会使用测试中的变量赋值模式,例如:
int bytesRead = 0;
while ((bytesRead = fs.Read(buffer, 0, buffer.Length)) > 0)
{
// ...
}
然而,上面使用的变量作用域模式并不是一种特别常见的代码模式,如果我看到它被到处使用,我会寻找一种方法来重构它。
是否有一些语法可以让我编写如下内容:
if (Dog dog = animal as Dog) { ... dog ... }
?
C# 6.0 中可能会有。此功能称为“声明表达式”。看
https://roslyn.codeplex.com/discussions/565640
详情。
建议的语法是:
if ((var i = o as int?) != null) { … i … }
else if ((var s = o as string) != null) { … s … }
else if ...
更一般地,建议的特性是局部变量声明可以用作表达式。这种 if
语法只是更通用功能的一个很好的结果。
if ((var myLocal = myObj.myProp) != null ) { … myLocal … }
我发现自己经常编写和使用的一种扩展方法*是
public static TResult IfNotNull<T,TResult>(this T obj, Func<T,TResult> func)
{
if(obj != null)
{
return func(obj);
}
return default(TResult);
}
在这种情况下可以用作
string name = (animal as Dog).IfNotNull(x => x.Name);
然后 name
是狗的名字(如果是狗),否则为空。
*我不知道这是否有效。它从来没有成为分析的瓶颈。
在这里违背粮食,但也许你一开始就做错了。检查对象的类型几乎总是一种代码味道。在您的示例中,不是所有动物都有名称吗?然后只需调用 Animal.name,而不检查它是否是狗。
或者,反转该方法,以便您在 Animal 上调用一个方法,该方法根据 Animal 的具体类型执行不同的操作。另见:多态性。
简短的陈述
var dog = animal as Dog
if(dog != null) dog.Name ...;
问题(语法)不在于赋值,因为 C# 中的赋值运算符是一个有效的表达式。相反,它带有所需的声明,因为声明是声明。
如果我必须编写这样的代码,我有时会(取决于更大的上下文)编写如下代码:
Dog dog;
if ((dog = animal as Dog) != null) {
// use dog
}
上述语法(接近要求的语法)有一些优点,因为:
在 if 之外使用 dog 会导致编译错误,因为它没有在其他地方赋值。 (也就是说,不要将 dog 分配到其他地方。)这种方法也可以很好地扩展到 if/else if/... (只有选择合适的分支所需的数量;这是我写的大案例当我必须时以这种形式。)避免重复 is/as。 (但也用 Dog dog = ... form 完成。)与“惯用的 while”没有什么不同。 (只是不要得意忘形:保持条件一致且简单。)
要真正将 dog
与世界其他地方隔离开来,可以使用一个新块:
{
Dog dog = ...; // or assign in `if` as per above
}
Bite(dog); // oops! can't access dog from above
快乐编码。
这里有一些额外的脏代码(虽然不像乔恩那样脏:-))依赖于修改基类。我认为它抓住了意图,但可能没有抓住重点:
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();
}
}
使用 C# 9.0 和 .NET 5.0,您可以这样编写它:
Animal animal;
if (animal as Dog is not null and Dog dog)
{
//You can get here only if animal is of type Dog and you can use dog variable only
//in this scope
}
这是因为 if 语句中的作为 Dog 的动物产生了与以下相同的结果:
animal is Dog ? (Dog)(animal) : (Dog)null
then is not null 部分检查上述语句的结果是否不为空。仅当此语句为真时,它才会创建 Dog dog 类型的变量,该变量不能为空。
C# 9.0 中带有模式组合器的此功能,您可以在此处阅读更多相关信息:https://docs.microsoft.com/pl-pl/dotnet/csharp/language-reference/proposals/csharp-9.0/patterns3#pattern-combinators
另一个迟到的条目:
if (animal is Dog dog)
{
dog.Name="Fido";
}
else if (animal is Cat cat)
{
cat.Name="Bast";
}
IDK 如果这对任何人都有帮助,但您始终可以尝试使用 TryParse 来分配您的变量。这是一个例子:
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;
}
总变量将在您的 if 语句之前声明。
你可以使用类似的东西
//声明变量 bool temp= false;
if (previousRows.Count > 0 || (temp= GetAnyThing()))
{
}
另一个带有扩展方法的 EVIL 解决方案:)
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;
}
}
我个人更喜欢干净的方式:
Dog dog = animal as Dog;
if (dog != null)
{
// do stuff
}
if 语句不允许这样做,但 for 循环会。
例如
for (Dog dog = animal as Dog; dog != null; dog = null)
{
dog.Name;
...
}
如果它的工作方式不是很明显,那么这里是该过程的逐步解释:
变量 dog 被创建为 dog 类型,并分配了转换为 Dog 的变量 animal。
如果赋值失败,则 dog 为空,这会阻止 for 循环的内容运行,因为它会立即中断。
如果赋值成功,则 for 循环将遍历迭代。
在迭代结束时,dog 变量被赋值为 null,这会跳出 for 循环。
using(Dog dog = animal as Dog)
{
if(dog != null)
{
dog.Name;
...
}
}
我知道我迟到了派对,但我想我会发布我自己的解决方法来解决这个困境,因为我还没有在这里(或任何地方)看到它。
/// <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;
}
}
}
有了这个,您可以执行以下操作:
if (animal.TryAs(out Dog dog))
{
//Do Dog stuff here because animal is a Dog
}
else
{
//Cast failed! animal is not a dog
}
重要说明:如果您想通过接口使用 TryAs(),则必须让该接口继承 IAble。
享受! 🙂
一些小实验表明我们可以在 if 语句中使用赋值
public static async Task Main(string[] args)
{
bool f = false;
if (f = Tru())
{
System.Diagnostics.Debug.WriteLine("True");
}
if (f = Tru(false))
{
System.Diagnostics.Debug.WriteLine("False");
}
}
private static bool Tru(bool t = true) => t ? true : false;
https://i.stack.imgur.com/EnDZ1.png
至于任何潜在的副作用或“邪恶”,我想不出任何,尽管我确信有人可以。欢迎评论!
使用 C# 7 的 Pattern Matching,您现在可以执行以下操作:
if (returnsString() is string msg) {
Console.WriteLine(msg);
}
这个问题是 10 多年前提出的,所以几乎所有其他答案都已过时/错误
有一种解决方法,我再举一个例子,你有一个返回字符串 ID 的方法,而不是 if 语句:
var userId = GetUserId();
if (!string.IsNullOrEmpty(userId))
{
//logic
}
你可能会期望这样的语法,这是行不通的:
if (string userId = GetUserId() && !string.IsNullOrEmpty(userId))
{
//logic
}
但是现在,您可以通过以下方式获得相同的结果:
if (GetUserId() is string userId && !string.IsNullOrEmpty(userId))
{
//logic
}
在您的示例中,您当然可以这样做:
if(animal is Dog dog)
{
//logic
}
但是考虑使用一种方法会很有用
var animal = GetAnimal();
if (animal is Dog)
{
//logic
}
最后您可以将其重写为:
if(GetAnimal() is Dog dog)
{
//logic
}
不定期副业成功案例分享
EVIL EVIL EVIL
背后的动机,但我并不积极。AsEither(...)
,我认为它比AsIf(...)
更清晰一些,所以我可以写myAnimal.AsEither(dog => dog.Woof(), cat => cat.Meeow(), unicorn => unicorn.ShitRainbows())
。