简体   繁体   中英

If I'm using non-nullable reference types, how do I show that I didn't find anything?

I've enabled the C# 8.0 non-nullable reference types feature in one of my projects, but now I'm unclear about how to represent missing data.

For example, I'm reading a file whose lines are colon-separated key/value pairs. Sometimes there's more than one colon on a line. In that case, the text before the first colon is the key, and the rest is the value. My code to parse each line looks like this:

public (string key, string value) GetKeyValue(string line)
{
    var split = line.Split(':');
    if (split.Length == 2)
        return (split[0].Trim(), split[1].Trim());
    else if (split.Length > 2)
    {
        var joined = string.Join(":", split.ToList().Skip(1));
        return (split[0].Trim(), joined.Trim());
    }
    else
    {
        Debug.Print($"Couldn't parse this into key/value: {line}");
        return (null, null);
    }
}

What this does: If we have just one colon, return the key and value. If we have more than one, join the rest of the text after the first colon, then return the key and value. Otherwise we have no colons and can't parse it, so return a null tuple. (Let's assume this last case can reasonably happen; I can't just throw and call it a bad file.)

Obviously that last line gets a nullability warning unless I change the declaration to

public (string? key, string? value) GetKeyValue(string line)

Now in F# I would just use an Option type and in the no-colon case, I'd return None.

But C# doesn't have an Option type. I could return ("", "") , but to me that doesn't seem better than nulls.

In a case like this, what's a good way to say "I didn't find anything" without using nulls?

You could include if the result was successful in parsing by just returning a flag:

public class Result
{
    private Result(){}

    public bool Successful {get;private set;} = false;

    public string Key {get; private set;} = string.Empty;

    public string Value {get; private set;} = string.Empty;

    public static Successful(string key, string value)
    {
        return new Result
        {
            Successful = true,
            Key = key,
            Value = value
        };
    }

    public static Failed()
    {
        return new Result();
    }
}

public Result GetKeyValue(string line){
     return Result.Failed();
}

Then you could use it like

var result = GetKeyValue("yoda");

if(result.Successful)
{
    // do something...
}

Alternatiely you could return 2 diffrent types and use pattern matching 👍

Actually, I realize now that part of the problem is that my method is doing two separate things:

  • Determine whether the line has a key.
  • Return the key and value.

Thus the return value has to indicate both whether there's a key and value, and what the key and value are.

I can simplify by doing the first item separately:

bool HasKey(string line)
{
    var split = line.Split(':');
    return split.Length >= 2;
}

Then in the method I posted, if there's no key, I can throw and say that the lines need to be filtered by HasKey first.

Putting on my functional thinking cap, an idiomatic return type would be IEnumerable<(string?,string?)> . The only change to your code would be to change return to yield return , and to remove the return statement if nothing is found.

public IEnumerable<(string? key, string? value)> GetKeyValue(string line)
{
    var split = line.Split(':');
    if (split.Length == 2)
        return (split[0].Trim(), split[1].Trim());
    else if (split.Length > 2)
    {
        var joined = string.Join(":", split.ToList().Skip(1));
        yield return (split[0].Trim(), joined.Trim());
    }
    else
    {
        Debug.Print($"Couldn't parse this into key/value: {line}");
    }
}

The caller then has several options on how to handle the response.

If they want to check if the key was found the old-fashioned eway, do this:

var result = GetKeyValue(line).SingleOrDefault();
if (!result.HasValue) HandleKeyNotFound();

If they prefer to throw an exception if the key is not found, they'd do this:

var result = GetKeyValue(line).Single();

If they just want to be quiet about it they can use ForEach , which will use the key and value if they are found and simply do nothing if they are not:

foreach (var result in GetKeyValue(line)) DoSomething(result.Item1, result.Item2);

Also, for what it's worth, I'd suggest using KeyValuePair instead of a tuple, since it clearly communicates the purpose of the fields.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM