Home > OS >  How to properly use TryGetValue Method with a class as Key?
How to properly use TryGetValue Method with a class as Key?

Time:01-10

My question is relative to the use of a dictionary. I will try to attach the code and then to explain my doubt:

    var ambassadors = new Dictionary<CountryCode, Ambassador>();
    Ambassador england = new Ambassador
    {
        CountryCode = new CountryCode("eng"),
        Name = "John",
        Age = 25

    };
    Ambassador australia = new Ambassador
    {
        CountryCode = new CountryCode("aus"),
        Name = "Martin",
        Age = 49

    };

    ambassadors.Add(england.CountryCode, england);
    ambassadors.Add(australia.CountryCode, australia);


    Console.WriteLine("Enter country code: ");
    var code = Console.ReadLine();

    if (ambassadors.TryGetValue(new CountryCode(code), out Ambassador ambassador))
    {
        Console.WriteLine($"The ambassador is {ambassador.Name}");
    }
    else
    {
        Console.WriteLine("The ambassador with the given code does not exist in the dictonary");
    }

    Console.ReadLine();
}

public class Ambassador
{
    public CountryCode CountryCode { get; set; }
    public string Name { get; set; }
    public int Age { get; set; }
}

public class CountryCode
{
    public string Code { get; }

    public CountryCode(string code)
    {
        Code = code;
    }

    public override bool Equals(object obj)
    {
        if (obj == null)
        {
            return false;

        }
        if (!(obj is CountryCode))
        {
            return false;
        }
        return StringComparer.OrdinalIgnoreCase.Equals(this.Code, ((CountryCode)obj).Code);
    }
    public override int GetHashCode()
    {
        return StringComparer.OrdinalIgnoreCase.GetHashCode(this.Code);
    }

My doubt is related to the line where I use the

ambassadors.TryGetValue

method inside the if to check the key and give me back the value. My question is: Why should I create a new instance (or initialize) of the Class CountrySide inside the TryGetValue?
I mean an instance of the class already exist inside ambassadors Dictionary. So, why c# ask me to initialize a new one inside the TryGetValue method and not simply check if that one specific exist, without the "new" before CoutryCode(code)?
If you could clarify me this concept I would appreciate it really.

CodePudding user response:

IMHO you don't need CountryCode class, string type would be plenty

var ambassadors = new Dictionary<string, Ambassador>();

public class Ambassador
{
    public string CountryCode { get; set; }
    public string Name { get; set; }
    public int Age { get; set; }
}
....
Ambassador england = new Ambassador
    {
        CountryCode = "eng",
        Name = "John",
        Age = 25

    };

if (  ambassadors.TryGetValue(code, out var ambassador))
    {
        Console.WriteLine($"The ambassador is {ambassador.Name}");
    }
    else
    {
        Console.WriteLine($"The ambassador with the given code \"{code}\" does not exist in the dictonary");
    }

// or you can use this too

if (ambassadors.ContainsKey(code)) 
    {
        Console.WriteLine($"The ambassador is {ambassadors[code].Name}");
    }
    else
    {
       Console.WriteLine($"The ambassador with the given code \"{code}\" does not exist in the dictonary");
    }

if for some reasons you need CountryCode class, IMHO it is better to define a dictionary this way. In this case you probably don' t need a bool Equals(object obj) method of CountryCode class.

var ambassadors = new Dictionary<string, Ambassador>();

you can add item to dictionary this way

    Ambassador england = new Ambassador
    {
        CountryCode = new CountryCode("eng"),
        Name = "John",
        Age = 25

    };
    Ambassador australia = new Ambassador
    {
        CountryCode = new CountryCode("aus"),
        Name = "Martin",
        Age = 49

    };

    ambassadors.Add(england.CountryCode.Code, england);
    ambassadors.Add(australia.CountryCode.Code, australia);

all another code will be the same as I posted above

CodePudding user response:

The other answers and comments already elaborated on it: Your dictionary is keyed by CountryCode and therefore needs a CountryCode instance, hence the requirement for new in

ambassadors.TryGetValue(new CountryCode(code), out var ambassador)

I would like to comment on a different angle here: While you can make the dictionary keyed by string and then use

var ambassadors = new Dictionary<string, Ambassador>(StringComparer.OrdinalIgnoreCase);
// ...
ambassadors.TryGetValue(code, out var ambassador)

directly, it needs to leak knowledge about how country codes are compared - note the StringComparer.OrdinalIgnoreCase in there.

I believe there may be a benefit in changing CountryCode to a struct instead:

public readonly struct CountryCode
{
    public string Code { get; }
    // ...
}

This will keep all validation code in place - i.e., the type constructor can still check whether something is a valid country code, not null or whitespace, etc. - but it will save resources on the allocation: Since a struct is a value type, new allocates on the stack rather than on the heap and is therefore virtually free as far as your usage here is concerned.

The crucial part - and you already have that - is to have Equals and GetHashCode set up. As mentioned above, the Equals is important here as it allows to treat treats both eng and ENG as similar. Given that, any type keyed by a CountryCode does not need to know or care about the details of equating two country codes, which is a strong benefit over a string key.

For usability, I would recommend to also implement the IEquatable<CountryCode> interface, as well as the operator == and operator != overloads:

public readonly struct CountryCode : IEquatable<CountryCode>
{
    // Note: `default(CountryCode)` will not call the constructor!
    private readonly string? _code;
    
    public CountryCode(string code)
    {
        // Verify country codes, throw exceptions, ...
        _code = code?.Trim().ToLowerInvariant() ?? throw new ArgumentNullException(nameof(code));
    }

    // Note: ensure proper values for `default(CountryCode)`.
    public string Code => _code ?? string.Empty; 

    public bool Equals(CountryCode other) =>
        StringComparer.OrdinalIgnoreCase.Equals(Code, other);

    public override bool Equals(object? obj) =>
        obj is CountryCode other && Equals(other);

    public override int GetHashCode() =>
        StringComparer.OrdinalIgnoreCase.GetHashCode(Code);

    public static bool operator ==(CountryCode lhs, CountryCode rhs) => 
        lhs.Equals(rhs);

    public static bool operator !=(CountryCode lhs, CountryCode rhs) => 
        !lhs.Equals(rhs);
}

If you're dealing with with common names, I had some successes with making the constructor private and naming out the values like so:

public readonly struct CountryCode : IEquatable<CountryCode>
{
    public static readonly CountryCode Empty = default;
    public static readonly CountryCode England = new("eng");
    public static readonly CountryCode Australia = new("aus");

    // ...
}

Most deserialization code would still pick up the private constructor, but any application code would be forced to use allowed values. This may or may not be useful for your case.

Note that could also use a string conversion operator like this:

public static implicit operator string(CountryCode code) =>
    code.Code;

Being explicit (var code = countryCode.Code) is usually better than being implicit (var code = countryCode) though, so take it with a grain of salt.

So my recommendation here is:

  • stick with the CountryCode key,
  • make CountryCode a readonly struct instead of a class, this makes it better and faster
  • keep an eye out for default(CountryCode) instances, as they will not have the constructor called; in class types, the entire value would be null then.
  •  Tags:  
  • Related