具有无意义默认值的结构


12

在我的系统经常与机场代码(操作"YYZ""LAX""SFO"等),他们总是在完全一样的格式(3封信,表示为大写)。系统通常每个API请求处理这些代码中的25-50个(不同),总共分配了上千个代码,它们在我们应用程序的许多层中传递,并且经常比较它们的相等性。

我们从传递字符串开始就可以了,但仍然可以正常工作,但很快就发现了很多编程错误,因为在期望3位代码的地方传递了错误的代码。我们还遇到了一些问题,我们应该进行不区分大小写的比较,而没有这样做,从而导致错误。

由此,我决定停止传递字符串并创建一个Airport类,该类具有一个采用并验证机场代码的构造函数。

public sealed class Airport
{
    public Airport(string code)
    {
        if (code == null)
        {
            throw new ArgumentNullException(nameof(code));
        }

        if (code.Length != 3 || !char.IsLetter(code[0]) 
        || !char.IsLetter(code[1]) || !char.IsLetter(code[2]))
        {
            throw new ArgumentException(
                "Must be a 3 letter airport code.", 
                nameof(code));
        }

        Code = code.ToUpperInvariant();
    }

    public string Code { get; }

    public override string ToString()
    {
        return Code;
    }

    private bool Equals(Airport other)
    {
        return string.Equals(Code, other.Code);
    }

    public override bool Equals(object obj)
    {
        return obj is Airport airport && Equals(airport);
    }

    public override int GetHashCode()
    {
        return Code?.GetHashCode() ?? 0;
    }

    public static bool operator ==(Airport left, Airport right)
    {
        return Equals(left, right);
    }

    public static bool operator !=(Airport left, Airport right)
    {
        return !Equals(left, right);
    }
}

这使我们的代码更易于理解,并且简化了相等性检查,字典/集合用法。现在我们知道,如果我们的方法接受一个Airport实例,它将按照我们期望的方式运行,那么它将方法检查简化为空引用检查。

但是,我确实注意到的是,垃圾收集运行的频率更高,我追踪了许多Airport收集垃圾的实例。

我对此的解决方案是将转换classstruct。主要是它只是一个关键字变化,例外GetHashCodeToString

public override string ToString()
{
    return Code ?? string.Empty;
}

public override int GetHashCode()
{
    return Code?.GetHashCode() ?? 0;
}

处理使用情况default(Airport)

我的问题:

  1. Airport通常是创建一个类或构造一个好的解决方案,还是我通过创建类型来解决错误的问题/以错误的方式解决它?如果不是一个好的解决方案,那么什么是更好的解决方案?

  2. 我的应用程序应如何处理使用的实例default(Airport)?一种类型的代码default(Airport)对我的应用程序毫无意义,因此我一直if (airport == default(Airport) { throw ... }在获取Airport(及其Code属性)实例对操作至关重要的地方进行操作。

注意:我查看了C#/ VB结构问题–如何避免零默认值的情况(对于给定的结构无效)?,以及在问我的问题之前是否使用struct,但是我认为我的问题足够不同,因此可以自己发表。


7
垃圾回收会对您的应用程序执行方式产生重大影响吗?换句话说,这有关系吗?
罗伯特·哈维

无论如何,是的,类解决方案是一个“好的”解决方案。您知道的方式是无需创建任何新问题即可解决您的问题。
罗伯特·哈维

2
解决default(Airport)问题的一种方法是简单地禁止默认实例。您可以通过编写一个参数的构造函数和投掷做,InvalidOperationExceptionNotImplementedException在里面。
罗伯特·哈维

3
附带说明一下,为什么不只将初始化字符串确认为3个字母字符,还不将其与所有机场代码的有限列表(例如github.com/datasets/airport-codes或类似代码)进行比较?
Dan Pichelman

2
我愿意打赌,这不是性能问题的根源。普通笔记本电脑可以按10M对象/秒的顺序进行分配。
Esben Skov Pedersen

Answers:


6

更新:我重写了我的答案,以解决关于C#结构的一些错误假设,以及OP在注释中通知我们正在使用内部字符串。


如果您可以控制进入系统的数据,请使用问题中所张贴的类。如果有人跑步,default(Airport)他们将获得null回报。请确保编写您的私有Equals方法,以便在比较空的Airport对象时返回false,然后让NullReferenceException代码飞到代码的其他位置。

但是,如果您要从不受控制的源将数据带入系统,则不必使整个线程崩溃。在这种情况下,结构是简单事实的理想选择,default(Airport)它将为您提供除null指针之外的其他功能。组成一个明显的值来表示“无值”或“默认值”,这样您就可以在屏幕上或日志文件中打印一些内容(例如“-”)。实际上,我只是保留code私有性,而根本不公开Code财产,而只是在此关注行为。

public struct Airport
{
    private string code;

    public Airport(string code)
    {
        // Check `code` for validity, throw exceptions if not valid

        this.code = code;
    }

    public override string ToString()
    {
        return code ?? (code = "---");
    }

    // int GetHashcode()

    // bool Equals(...)

    // bool operator ==(...)

    // bool operator !=(...)

    private bool Equals(Airport other)
    {
        if (other == null)
            // Even if this method is private, guard against null pointers
            return false;

        if (ToString() == "---" || other.ToString() == "---")
            // "Default" values should never match anything, even themselves
            return false;

        // Do a case insensitive comparison to enforce logic that airport
        // codes are not case sensitive
        return string.Equals(
            ToString(),
            other.ToString(),
            StringComparison.InvariantCultureIgnoreCase);
    }
}

与其他有效的机场代码相比,更糟糕的情况转换default(Airport)为字符串会打印出来"---"并返回false。任何“默认”机场代码均不匹配,包括其他默认机场代码。

是的,结构是要在堆栈上分配的值,指向堆内存的任何指针基本上都抵消了结构的性能优势,但是在这种情况下,结构的默认值具有含义,并为其余结构提供了额外的防弹性能应用。

因此,我在这里稍微修改一下规则。


原始答案(有一些事实错误)

如果可以控制进入系统的数据,我将按照Robert Harvey在注释中的建议进行操作:创建无参数构造函数,并在调用它时引发异常。这样可以防止无效数据通过进入系统default(Airport)

public Airport()
{
    throw new InvalidOperationException("...");
}

但是,如果您要从不受控制的源将数据带入系统,则不必使整个线程崩溃。在这种情况下,您可以创建一个无效的机场代码,但使它看起来像一个明显的错误。这将涉及创建无参数构造函数并将设置Code为类似“ ---”的内容:

public Airport()
{
    Code = "---";
}

由于您使用a string作为代码,因此使用结构没有意义。该结构在堆栈上分配,只是Code分配为指向堆内存中字符串的指针,因此,类和结构之间没有区别。

如果将机场代码更改为3的char数组,则结构将在堆栈上完全分配。即使那样,数据量也没有太大的不同。


如果我的应用程序对Code属性使用了内部字符串,那是否会改变关于字符串在堆内存中的指向的理由?
马修(Matthew)

@Matthew:使用类会给您带来性能问题吗?如果不是,则掷硬币决定使用哪个硬币。
格雷格·伯格哈特

4
@Matthew:真正重要的是您集中了规范代码和比较的麻烦逻辑。在那之后,“类与结构”只是一个学术性的讨论,直到您对性能产生足够大的影响以证明有足够的开发人员时间进行学术性讨论为止。
格雷格·伯格哈特

1
没错,我不介意时不时进行学术讨论,以帮助我将来创建更明智的解决方案。
马修(Matthew)

@Matthew:是的,你是绝对正确的。他们说“谈话便宜”。当然,比不说话和做一些糟糕的事情要便宜。:)
Greg Burghardt

13

使用Flyweight模式

由于正确地,机场是不可变的,因此无需为一个特定实例(例如SFO)创建多个实例。使用Hashtable或类似的东西(注意,我是Java的人,不是C#,所以详细信息可能会有所不同),以在创建机场时缓存它们。在创建新表之前,请检查哈希表。您永远不会释放机场,因此GC无需释放它们。

一个额外的次要优势(至少在Java中,不确定C#)是您不需要编写equals()方法,只需简单地==做一下即可。相同hashcode()


3
flyweight模式的出色用法。
尼尔

2
假设OP一直使用结构而不是类,字符串实习生是否已经在处理可重用的字符串值?这些结构已经存在于堆栈中,字符串已经被重用,以避免在内存中重复值。轻量级飞行模式还能带来什么额外的好处?
扁平的

需要提防的东西。如果添加或删除了机场,则需要以一种刷新此静态列表的方式进行构建,而无需重新启动应用程序或重新部署它。不经常添加或删除机场,但是当简单的更改变得如此复杂时,企业主往往会感到不高兴。“我不能只是将它添加到某个地方吗?!为什么我们必须安排发布/应用程序重新启动并给客户带来不便?” 但是我也考虑首先要使用某种静态缓存。
格雷格·伯格哈特

@Flater合理点。我要说的是,初级程序员无需过多地考虑堆栈与堆的关系。再加上我的加法-无需编写equals()。
user949300

1
@Greg Burghardt如果getAirportOrCreate()代码正确同步,则没有技术原因,您无法在运行时根据需要创建新的Airports。可能有业务原因。
user949300

3

我不是一个特别高级的程序员,但这不是枚举的理想选择吗?

有多种方法可从列表或字符串构造枚举类。这是我过去所见过的,虽然不确定这是否是最好的方法。

https://blog.kloud.com.au/2016/06/17/converting-webconfig-values-into-enum-or-list/


2
如果可能有成千上万个不同的值(例如机场代码),则枚举不切实际。
Ben Cottrell

是的,但是我发布的链接是如何将字符串作为枚举加载。这是另一个将查询表加载为枚举的链接。这可能需要一些工作,但会利用枚举的功能。exceptionnotfound.net/...
亚当乙

1
或者可以从数据库或文件中加载有效代码列表。然后,仅检查机场代码是否在该列表中。当您不再希望对值进行硬编码和/或列表变得难以管理时,通常会这样做。
尼尔

@BenCottrell正是代码生成和T4模板的用途。
RubberDuck

3

您看到更多GC活动的原因之一是因为您现在正在创建第二个字符串- .ToUpperInvariant()原始字符串的版本。原始字符串在构造函数运行后立即可以使用GC,第二个字符串与Airport对象同时可以使用。您可能可以通过其他方式将其最小化(请注意的第三个参数string.Equals()):

public sealed class Airport : IEquatable<Airport>
{
    public Airport(string code)
    {
        if (code == null)
        {
            throw new ArgumentNullException(nameof(code));
        }

        if (code.Length != 3 || !char.IsLetter(code[0])
                             || !char.IsLetter(code[1]) || !char.IsLetter(code[2]))
        {
            throw new ArgumentException(
                "Must be a 3 letter airport code.",
                nameof(code));
        }

        Code = code;
    }

    public string Code { get; }

    public override string ToString()
    {
        return Code; // TODO: Upper-case it here if you really need to for display.
    }

    public bool Equals(Airport other)
    {
        return string.Equals(Code, other?.Code, StringComparison.InvariantCultureIgnoreCase);
    }

    public override bool Equals(object obj)
    {
        return obj is Airport airport && Equals(airport);
    }

    public override int GetHashCode()
    {
        return Code.GetHashCode();
    }

    public static bool operator ==(Airport left, Airport right)
    {
        return Equals(left, right);
    }

    public static bool operator !=(Airport left, Airport right)
    {
        return !Equals(left, right);
    }
}

对于相同(但大小写不同)的机场,这不会产生不同的哈希码吗?
Hero Wanders

是的,我会这样想。该死的。
Jesse C. Slicer,

这是一个很好的观点,没想过,我来看看进行这些更改。
马修(Matthew)

1
关于GetHashCode,应该使用StringComparer.OrdinalIgnoreCase.GetHashCode(Code)或类似的方法
Matthew
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.