如何正确地检查空值?

122

我喜欢空合并运算符,因为它可以轻松地为可空类型分配默认值。

 int y = x ?? -1;

很不错,但如果我需要对x进行一些简单的操作怎么办呢。例如,如果我想检查 Session,那么通常我就要写更冗长的代码。

我希望我能这样做:

string y = Session["key"].ToString() ?? "none";

但是你不能这样做,因为在空值检查之前会调用.ToString()方法,所以如果Session["key"]为空,它将会失败。我最终会这样做:

string y = Session["key"] == null ? "none" : Session["key"].ToString();

它运行良好,而且我认为比三行代码的替代方案更好:

string y = "none";
if (Session["key"] != null)
    y = Session["key"].ToString();

尽管那样做是可行的,但我仍然想知道是否有更好的方法。似乎无论如何我总是需要两次引用Session["key"];一次用于检查,另一次用于赋值。有什么想法吗?


20
这就是我希望C#有一个“安全导航运算符”(.?),就像Groovy一样。 - Cameron
2
@Cameron:这时我希望C#能将可空类型(包括引用类型)视为单子,这样就不需要“安全导航符”了。 - Jon Purdy
3
空引用的发明者称其为“亿美元错误”,我倾向于同意这种说法。请参见http://www.infoq.com/presentations/Null-References-The-Billion-Dollar-Mistake-Tony-Hoare。 - Jamie Ide
他实际的错误是不安全(非语言强制)混合可空和非可空类型。 - MSalters
10个回答

182

那么

string y = (Session["key"] ?? "none").ToString();

2
@Matthew:不是的,因为Session值的类型是Object。 - BlackBear
1
@BlackBear 但是返回的值很可能是一个字符串,所以强制转换是有效的。 - Firo
10
我不喜欢这样做,因为如果你在会话中放入了与预期不同类型的对象,你可能会掩盖程序中一些微妙的错误。我更愿意使用安全转换,因为我认为它更有可能快速发现错误。它还避免了在字符串对象上调用ToString()方法。 - tvanfosson
@tvanfosson:我不确定我理解了。你能提供一个例子吗? - BlackBear
Session["key"] = 10; string y = (Session["key"] ?? "none").ToString(); 当执行这段代码时,y的值会变成字符串"10"。但实际上,它应该抛出一个异常或者获取默认值,可能是前者。至少你应该能够区分这一点和Session["key"] = "10"; string y = (Session["key"] ?? "none").ToString();之间的区别。 - tvanfosson
显示剩余5条评论

130

如果你经常这样做,特别是使用 ToString(),那么你可以编写扩展方法:

public static string NullPreservingToString(this object input)
{
    return input == null ? null : input.ToString();
}

...

string y = Session["key"].NullPreservingToString() ?? "none";
当然也可以使用带有默认值的方法:
public static string ToStringOrDefault(this object input, string defaultValue)
{
    return input == null ? defaultValue : input.ToString();
}

...

string y = Session["key"].ToStringOrDefault("none");

StackExchange的DataExplorer有一个类似于这个的扩展方法,它还具有多个默认值的好处。string IsNullOrEmptyReturn(this string s, params string[] otherPossibleResults)http://code.google.com/p/stack-exchange-data-explorer/source/browse/App/StackExchange.DataExplorer/ExtensionMethods.cs#84 - David Murdoch
7
我完全无法同意这个观点。在“object”上使用扩展方法会给代码库带来麻烦和垃圾代码,对空的“this”值操作而不报错的扩展方法则是极其恶劣的。 - Nick Larsen
10
@NickLarsen:我认为适度是关键。在我的看法中,能够处理空值的扩展方法非常有用——只要它们清楚地说明自己的功能。 - Jon Skeet

21

你还可以使用as,如果转换失败,则返回null

Session["key"] as string ?? "none"
这将返回"none",即使有人在Session["key"]中存储了一个int

1
只有在第一次使用时不需要 ToString() 才能正常工作。 - Abel
1
我很惊讶还没有人对这个答案进行踩票。这与OP想要做的完全不同。 - Timwi
@Timwi:OP使用ToString()将包含字符串的对象转换为字符串。您可以使用obj as string(string)obj来执行相同的操作。这在ASP.NET中是一个非常常见的情况。 - Andomar
5
@Andomar:不,原帖中是在一个对象(即Session["key"])上调用了ToString() 方法,但他并没有提到这个对象的类型。它可能是任何类型的对象,不一定是字符串。 - Timwi

13

如果它始终是一个string,你可以进行类型转换:

string y = (string)Session["key"] ?? "none";

如果有人把一个 int 或其他东西放进了 Session["key"] 中,这种方式会提出警告而不是隐藏错误。 ;)


10
所有提出的解决方案都很好,并回答了问题;所以这只是稍微扩展一下。目前大部分答案只涉及null验证和字符串类型。您可以通过扩展StateBag对象来包含类似于Jon Skeet发布的答案的通用GetValueOrDefault方法。
一个简单的通用扩展方法,接受一个字符串作为键,然后类型检查会话对象。如果对象为空或类型不同,则返回默认值;否则会强类型返回会话值。
像这样:
/// <summary>
/// Gets a value from the current session, if the type is correct and present
/// </summary>
/// <param name="key">The session key</param>
/// <param name="defaultValue">The default value</param>
/// <returns>Returns a strongly typed session object, or default value</returns>
public static T GetValueOrDefault<T>(this HttpSessionState source, string key, T defaultValue)
{
    // check if the session object exists, and is of the correct type
    object value = source[key]
    if (value == null || !(value is T))
    {
        return defaultValue;
    }

    // return the session object
    return (T)value;
}

1
你能为这个扩展方法提供一个使用示例吗?StateBag不是处理视图状态而不是会话吗?我正在使用ASP.NET MVC 3,所以我没有简单地访问视图状态。我认为你想要扩展HttpSessionState - Chev
这个问题需要检索值3x和2个转换,如果成功的话。 (我知道这是一个字典,但初学者可能会在昂贵的方法上使用类似的做法。) - Jake Berger
3
T value = source[key] as T; return value ?? defaultValue;翻译:将source中key对应的值赋给T类型的变量value,如果value为null,则返回defaultValue。 - Jake Berger
1
@jberger 使用 "as" 进行值转换是无法访问的,因为泛型类型没有类约束,可能需要返回像布尔值这样的值。@AlexFord 抱歉,您需要扩展 HttpSessionState 来处理会话。 :) - Richard
确实。正如Richard所指出的那样,需要使用约束条件。(如果您想使用值类型,则可以使用另一种方法) - Jake Berger

7
我们使用一种叫做NullOr的方法。

用法

// Call ToString() if it’s not null, otherwise return null
var str = myObj.NullOr(obj => obj.ToString());

// Supply default value for when it’s null
var str = myObj.NullOr(obj => obj.ToString()) ?? "none";

// Works with nullable return values, too —
// this is properly typed asint?” (nullable int)
// even ifCount” is just int
var count = myCollection.NullOr(coll => coll.Count);

// Works with nullable input types, too
int? unsure = 47;
var sure = unsure.NullOr(i => i.ToString());

源码

/// <summary>Provides a function delegate that accepts only value types as return types.</summary>
/// <remarks>This type was introduced to make <see cref="ObjectExtensions.NullOr{TInput,TResult}(TInput,FuncStruct{TInput,TResult})"/>
/// work without clashing with <see cref="ObjectExtensions.NullOr{TInput,TResult}(TInput,FuncClass{TInput,TResult})"/>.</remarks>
public delegate TResult FuncStruct<in TInput, TResult>(TInput input) where TResult : struct;

/// <summary>Provides a function delegate that accepts only reference types as return types.</summary>
/// <remarks>This type was introduced to make <see cref="ObjectExtensions.NullOr{TInput,TResult}(TInput,FuncClass{TInput,TResult})"/>
/// work without clashing with <see cref="ObjectExtensions.NullOr{TInput,TResult}(TInput,FuncStruct{TInput,TResult})"/>.</remarks>
public delegate TResult FuncClass<in TInput, TResult>(TInput input) where TResult : class;

/// <summary>Provides extension methods that apply to all types.</summary>
public static class ObjectExtensions
{
    /// <summary>Returns null if the input is null, otherwise the result of the specified lambda when applied to the input.</summary>
    /// <typeparam name="TInput">Type of the input value.</typeparam>
    /// <typeparam name="TResult">Type of the result from the lambda.</typeparam>
    /// <param name="input">Input value to check for null.</param>
    /// <param name="lambda">Function to apply the input value to if it is not null.</param>
    public static TResult NullOr<TInput, TResult>(this TInput input, FuncClass<TInput, TResult> lambda) where TResult : class
    {
        return input == null ? null : lambda(input);
    }

    /// <summary>Returns null if the input is null, otherwise the result of the specified lambda when applied to the input.</summary>
    /// <typeparam name="TInput">Type of the input value.</typeparam>
    /// <typeparam name="TResult">Type of the result from the lambda.</typeparam>
    /// <param name="input">Input value to check for null.</param>
    /// <param name="lambda">Function to apply the input value to if it is not null.</param>
    public static TResult? NullOr<TInput, TResult>(this TInput input, Func<TInput, TResult?> lambda) where TResult : struct
    {
        return input == null ? null : lambda(input);
    }

    /// <summary>Returns null if the input is null, otherwise the result of the specified lambda when applied to the input.</summary>
    /// <typeparam name="TInput">Type of the input value.</typeparam>
    /// <typeparam name="TResult">Type of the result from the lambda.</typeparam>
    /// <param name="input">Input value to check for null.</param>
    /// <param name="lambda">Function to apply the input value to if it is not null.</param>
    public static TResult? NullOr<TInput, TResult>(this TInput input, FuncStruct<TInput, TResult> lambda) where TResult : struct
    {
        return input == null ? null : lambda(input).Nullable();
    }
}

是的,这是更通用的解决问题的答案 - 你比我先想到了 - 并且是安全导航的候选方案(如果你不介意使用lambda表达式来处理简单的事情) - 但是它仍然有点繁琐,需要写很多代码 : )。就个人而言,我总是会选择使用 ? : 运算符(如果不昂贵的话,如果昂贵的话,那么无论如何都要重新排列代码)。 - NSGaga-mostly-inactive
...而“命名”是这个问题的真正难点 - 没有什么能真正准确地描述(或者“添加”的太多),或者名称太长了 - NullOr很好,但在我看来过于强调“null”(而且你还有 ??)- 我使用的是“Property”或“Safe”。
value.Dot(o=>o.property) ?? @default
也许?
- NSGaga-mostly-inactive
@NSGaga:我们在名称上反复考虑了很长时间。我们确实考虑过 Dot,但发现它太不具描述性了。我们选择了 NullOr 作为自我解释和简洁之间的良好折衷方案。如果你真的不关心命名,你总是可以称其为 _。如果你觉得写 lambda 表达式太麻烦,你可以使用代码片段,但我个人认为这已经足够容易了。至于 ? :,你不能在更复杂的表达式中使用它,你必须将它们移动到一个新的本地变量中;而 NullOr 可以避免这种情况。 - Timwi

6

如果只需要一次转换,我的建议是使用安全的字符串转换,以防存储在键中的对象不是字符串。使用ToString()可能得不到您想要的结果。

var y = Session["key"] as string ?? "none";

正如@Jon Skeet所说,如果你发现自己经常这样做,可以使用扩展方法或更好的方式是结合强类型的SessionWrapper类。即使没有使用扩展方法,强类型的包装器也是一个不错的选择。
public class SessionWrapper
{
    private HttpSessionBase Session { get; set; }

    public SessionWrapper( HttpSessionBase session )
    {
        Session = session;
    }

    public SessionWrapper() : this( HttpContext.Current.Session ) { }

    public string Key
    {
         get { return Session["key"] as string ?? "none";
    }

    public int MaxAllowed
    {
         get { return Session["maxAllowed"] as int? ?? 10 }
    }
}

用作

 var session = new SessionWrapper(Session);

 string key = session.Key;
 int maxAllowed = session.maxAllowed;

3
创建一个辅助函数。
public static String GetValue( string key, string default )
{
    if ( Session[ key ] == null ) { return default; }
    return Session[ key ].toString();
}


string y = GetValue( 'key', 'none' );

2

Skeet的答案是最好的 - 特别是我认为他的ToStringOrNull()非常优雅,最适合您的需求。我想在扩展方法列表中添加另一种选项:

返回原始对象或空值的默认字符串值:

// Method:
public static object OrNullAsString(this object input, string defaultValue)
{
    if (defaultValue == null)
        throw new ArgumentNullException("defaultValue");
    return input == null ? defaultValue : input;
}

// Example:
var y = Session["key"].OrNullAsString("defaultValue");

使用 var 作为返回值,因为它将以原始输入的类型返回,仅当 null 时作为默认字符串。


如果不需要(即input!= null),为什么在null defaultValue上抛出异常? - Attila
如果 input != null,则返回该对象本身。如果 input == null,则返回作为参数提供的字符串。因此,一个人可能会调用 .OnNullAsString(null),但是这个目的(尽管很少有用的扩展方法)是确保您要么得到对象,要么得到默认字符串... 永远不会是 null。 - one.beat.consumer
如果input!=null,则只有在defaultValue!=null的情况下才会返回输入;否则它将抛出一个ArgumentNullException - Attila

0

这是我的小型类型安全的 "Elvis 操作符",适用于不支持 ? 的 .NET 版本。

public class IsNull
{
    public static O Substitute<I,O>(I obj, Func<I,O> fn, O nullValue=default(O))
    {
        if (obj == null)
            return nullValue;
        else
            return fn(obj);
    }
}

第一个参数是被测试的对象,第二个参数是函数,第三个参数是空值。所以对于你的情况:

IsNull.Substitute(Session["key"],s=>s.ToString(),"none");

对于可空类型也非常有用。例如:

decimal? v;
...
IsNull.Substitute(v,v.Value,0);
....

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接