为什么IsNan是Double类的静态方法而不是实例属性?

12
标题中的问题是:为什么?
return double.IsNaN(0.6d) && double.IsNaN(x);

与其

return (0.6d).IsNaN && x.IsNaN;

我这么说是因为当实现自定义结构体,其具有与 NaN 相同含义的特殊值时,我倾向于使用第二种方法。

此外,该属性的性能通常较好,因为它避免了在堆栈上复制结构体以调用 IsNaN 静态方法(而且由于我的属性不是虚拟的,所以不存在自动装箱的风险)。尽管对于内置类型来说这并不是真正的问题,因为 JIT 可以轻松优化这个过程。

我目前最好的猜测是,由于在 double 类中不能同时使用属性和静态方法来表示相同的名称,所以他们更喜欢 Java 风格的语法。(事实上,你可以同时定义 get_IsNaN 属性 getter 和 IsNaN 静态方法,但在任何支持属性语法的 .Net 语言中,这将会很令人困惑。)


1
我同意这有点奇怪 - 还有很多其他类似的情况,例如Char.IsDigit等。我从未见过一个好的解释:( - Jon Skeet
7个回答

14

静态方法是线程安全的,原始类型的方法通常需要是线程安全的,以支持平台上的线程(至少应该安全地避免内部竞争条件),实例方法需要使用管理指向结构的指针,这意味着在方法执行时可以并发修改结构/原语。另一方面,静态方法会复制结构/原语,因此不会出现线程竞争条件。

如果结构体被设计为线程安全,则仅当方法执行原子操作时,它们应该被制作为实例方法,否则应选择静态方法。

(作为另一种选择,可以使用使用锁定的实例方法,但它们比复制更昂贵)

编辑:@VirtualBlackFox 我已经准备了一个示例,以表明对于不可变结构体,即使在实例方法中也不是线程安全的:

using System;
using System.Threading;

namespace CA64213434234
{
    class Program 
    {
        static void Main(string[] args)
        {
            ManualResetEvent ev = new ManualResetEvent(false);
            Foo bar = new Foo(0);
            Action a =  () => bar.Display(ev);
            IAsyncResult ar = a.BeginInvoke(null, null);
            ev.WaitOne();
            bar = new Foo(5);
            ar.AsyncWaitHandle.WaitOne();
        }
    }

    public struct Foo
    {
        private readonly int val;
        public Foo(int value)
        {
            val = value;
        }
        public void Display(ManualResetEvent ev)
        {
            Console.WriteLine(val);
            ev.Set();
            Thread.Sleep(2000);
            Console.WriteLine(val);
        }
    }
}

显示实例方法打印: 0 5

即使结构是不可变的。要使用线程安全的方法,请使用静态方法。


有任何支持这个的引用吗? - Ruben Bartelink
1
@Ruben Bartelink,没有引用,只是一般的CLR知识,请阅读Jeffrey Richter的《通过C#了解CLR》,他解释了我所说的所有内容。 - Pop Catalin
显然我太久没看过它了(还有Applid .NET和Box and Skeet,我很骄傲...)。点个赞吧! - Ruben Bartelink
这是我认为的一个很好的理由,即使可以轻松地在实例方法中完成(没有什么阻止实例方法在本地副本上工作)。而且,由于我正在处理不可变数据结构,因此使用IsNaN属性而不是静态方法也让我感到舒适。 - Julien Roncaglia
我的意思是,如果实现了一个IsNaN属性,调用者并不关心它是否线程安全,因为有锁、本地副本或其他任何东西,他只关心它是否(或不是)线程安全。 - Julien Roncaglia
显示剩余11条评论

9

有趣的问题;不知道答案 - 但如果这个问题真正困扰您,您可以声明一个扩展方法,但它仍将使用堆栈等。

static bool IsNaN(this double value)
{
    return double.IsNaN(value);
}

static void Main()
{
    double x = 123.4;
    bool isNan = x.IsNaN();
}

如果C#有扩展属性会更好(语法上),但上述方法是目前最接近的,但无论如何都应该很好地“内联”。


更新;经过思考,静态方法和实例方法之间还有另一个区别;即使类型是密封的且不可为空,C#始终使用“callvirt”而不是“call”来调用实例方法。因此,使用静态方法可能会带来性能优势?幸运的是,扩展方法仍然被视为静态方法,因此您可以保留此行为。


1
扩展方法/属性还没有完全注册到我的大脑中..但是很棒的使用方法。 - Gishu
这个通用流畅扩展,而不是静态的,还让我创建了一个 string.IsEmpty 和 string.IsNullOrEmpty 作为实例。 - Ruben Bartelink
关于callvirt,正如其他地方所提到的,扩展方法和属性之间的主要区别在于在扩展方法中您可以检查this == null,而callvirt会检查null并抛出NullReferenceException。 - Ruben Bartelink
编译器永远不会为结构体上的方法发出“callvirt”,因为它知道它们不能是虚拟的,除非首先发出一个“box”操作数。 - Pop Catalin
1
@TheSoftwareJedi:只有涉及到“对象”时才会发生装箱。调用实例方法无需进行装箱。 - Marc Gravell
显示剩余3条评论

4

@Pop Catalin: 我对你在以下内容说的话感到不满意:

如果结构体旨在实现线程安全,则应仅使方法成为实例方法,仅当它们执行原子操作时,否则应选择静态方法。

这是一个小程序,演示了静态方法不能解决结构体中的问题:

using System;
using System.Threading;
using System.Diagnostics;

namespace ThreadTest
{
    class Program
    {
        struct SmallMatrix
        {
            double m_a, m_b, m_c, m_d;

            public SmallMatrix(double x)
            {
                m_a = x;
                m_b = x;
                m_c = x;
                m_d = x;
            }

            public static bool SameValueEverywhere(SmallMatrix m)
            {
                return (m.m_a == m.m_b)
                    && (m.m_a == m.m_c)
                    && (m.m_a == m.m_d);
            }
        }

        static SmallMatrix s_smallMatrix;

        static void Watcher()
        {
            while (true)
                Debug.Assert(SmallMatrix.SameValueEverywhere(s_smallMatrix));
        }

        static void Main(string[] args)
        {
            (new Thread(Watcher)).Start();
            while (true)
            {
                s_smallMatrix = new SmallMatrix(0);
                s_smallMatrix = new SmallMatrix(1);
            }
        }
    }
}

请注意,在普通处理器上,双精度浮点数的这种行为无法观察到,因为大多数x86指令都有一个适用于64位块的版本,例如movl
因此,线程安全似乎不是使IsNaN成为静态的好理由:
1. 框架应该是平台无关的,因此不应预设处理器体系结构之类的东西。IsNaN的线程安全性依赖于目标体系结构上始终以原子方式访问和修改64位值的事实(而Compact框架的目标不是x86…)。
2. IsNaN本身是无用的,并且在多个线程可以访问someVar的情况下,此代码无论如何都不安全(无论IsNaN的线程安全性如何)。
print("code sample");
if (!double.IsNaN(someVar))
    Console.WriteLine(someVar);

我的意思是,即使IsNaN使用所有可能的NaN值进行==比较实现...(不太可能)......如果方法在执行期间值发生变化,那么谁会在乎,反正方法终止时它可能已经改变了...或者它甚至可能是一个中间值,如果目标体系结构不是x86,则永远不应该出现这种情况...

通常情况下,在两个不同的线程中访问内置值是不安全的,因此,在处理结构体或任何其他类型时,我认为没有提供任何静态方法的虚假安全性 illusion有任何意义。


线程安全并不总是关于原子操作,而是关于1)不创建竞争条件(即不修改共享状态),2)不受其影响(即不使用共享状态)。静态方法同时满足1)和2)。接受值的静态方法是线程安全的... - Pop Catalin
读取值和写入值并不会引起竞争,竞争是在您的应用程序中引入的,而不是在静态方法中。 - Pop Catalin
静态方法保证调用者不会在背后修改结构体,但我没有看到IsNaN修改状态的任何实现。我同意你的1,只是我不明白为什么它可以应用于这里。 - Julien Roncaglia
如之前所述,我的问题与您的2有关:谁在乎IsNaN是否在其内部受到竞态条件的保护? - Julien Roncaglia
鉴于在x86系统上双精度值不会发生这种情况,因为它们在一个原子操作中被复制到堆栈中。但这取决于架构,并不适用于结构体。 - Julien Roncaglia
显示剩余6条评论

0

实例和静态之间的区别是C#语言(以及您所说的Java)选择明确的基本要点(在C++中,您可以通过实例调用静态方法,但那只是语法 - 在幕后,instance.StaticX与instance Class.StaticX是相同的)。

然而,朝着流畅接口的发展已经开始揭示了很多这方面的问题...


是的,马克,对不起VBF,在我的初始回复中犯了一个大错误——没有意识到你们已经很好地解决了这个问题,并且在考虑比我更深入的事情!(我的编辑使你的观点不太清晰,但它肯定是正确的——我的错!) - Ruben Bartelink

0

我记得一位导师说过,任何不使用除参数以外的其他变量的方法都应该是静态方法。

我真的不知道为什么,也没有想过背后的原因,但从逻辑上看,这似乎是个好主意。无论如何,我对答案很感兴趣;-)


我记得Scott Meyers说过(虽然是关于C++的)。无论这是否与某种性能或技术问题有关,还是只是“品味” - 我记不清了;-) - Christian.K
调用带有对象参数的静态方法或实例参数的无参方法应该完全相同。唯一不应该做的事情是一个根本没有使用实例的实例方法(即使在某些情况下也可能需要这样做)。 - Julien Roncaglia

0

我认为Marc已经找到了答案。

问题在于当您需要在值类型上调用实例方法时,该值将被装箱。这会导致严重的性能损失。


错误,阅读 ECMA-335(CIL)第13.3章:“此指针”。相比之下,值类型的实例和虚拟方法应编码为期望管理指向值类型未打包实例的指针(请参见第一部分)。 - Julien Roncaglia

0

Double.IsNan与String.IsNullorEmpty遵循相同的模式。后者的行为是因为遗憾地没有办法声明非虚拟实例方法可与null “this”一起使用。虽然这种行为对于可变引用类型来说可能很奇怪,但对于必须是引用类型但在语义上应该像不可变值一样行事的东西来说,这将非常有用。例如,“String”类型如果在空对象上调用其属性会表现得与在空字符串上调用它们完全相同,则“String”类型将更加方便。事实上,在将null字符串对象视为空字符串的上下文中存在一些奇怪的混乱情况,而在尝试使用一个时会生成错误的上下文中也是如此。如果string作为一个初始化为空字符串的值类型始终保持一致性,那么它会更加干净。


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