为什么这个 C# 函数使用 System.Drawing 溢出来绘制正弦波?

Why does this C# function to draw a sine wave using System.Drawing overflows?

所以我制作了这个绘制正弦波的小 windows 表单程序,但是当我输入某些函数(如 sin(x) ^ x 时,它会溢出。 请查看此代码并帮助我解决这个问题!

    private void button1_Click(object sender, EventArgs e)
    {
        if (Completed) return;
        Completed = true;
        Graphics g = this.CreateGraphics();
        Pen pen = new Pen(Brushes.Black, 2.0F);

        float x1 = 0;
        float y1 = 0;

        float y2 = 0;

        float yEx = 300;
        float eF = 50;


        for (float x = 0; x < Width; x += 0.005F)
        {
            y2 = (float)Math.Pow(Math.Sin(x) , x);

            g.DrawLine(pen, x1 * eF, y1 * eF + yEx, x * eF, y2 * eF + yEx);
            x1 = x;
            y1 = y2;
        }

    }

当我执行时,它运行了一会儿然后显示:

感谢任何贡献!

验证 DrawLine 的值。

您在循环中检查了 Width,但在调用 DrawLine 之前没有验证您的计算。这是一个没有平局的例子:

float x1 = 0;
float y1 = 0;

float y2 = 0;

float yEx = 300;
float eF = 50;

const int width = 1024;
const int height = 1024;

for (float x = 0; x < width; x += 0.005F)
{
    y2 = (float)Math.Pow(Math.Sin(x) , x);

    var a = x1 * eF;
    var b = y1 * eF + yEx; 
    var c = x * eF;
    var d = y2 * eF + yEx;

    if(a < 0 || a >= width || b < 0 || b >= height || c < 0 || c >= width || d < 0 || d > height)
        Console.WriteLine("Snap! {0} | {1} | {2} | {3}", a, b, c, d);
    x1 = x;
    y1 = y2;
}

Here's an online example

当您尝试绘制 canvas 时,GDI 不会帮助您,实际上它通常会爆炸。您应该始终将值限制在高度和宽度范围内。

以下是示例的一些要点:

Snap! 1101,215 | NaN | 1101,465 | NaN
Snap! 1101,465 | NaN | 1101,715 | NaN
Snap! 1101,715 | NaN | 1101,965 | NaN
Snap! 1101,965 | NaN | 1102,215 | NaN
Snap! 1102,215 | NaN | 1102,465 | NaN
Snap! 1102,465 | NaN | 1102,715 | NaN
Snap! 1102,715 | NaN | 1102,965 | NaN
Snap! 1102,965 | NaN | 1103,215 | NaN
Snap! 1103,215 | NaN | 1103,465 | NaN
Snap! 1103,465 | NaN | 1103,715 | NaN
Snap! 1103,715 | NaN | 1103,965 | NaN
Snap! 1103,965 | NaN | 1104,215 | NaN
Snap! 1104,215 | NaN | 1104,465 | NaN
Snap! 1104,465 | NaN | 1104,715 | NaN
Snap! 1104,715 | NaN | 1104,965 | NaN
Snap! 1104,965 | NaN | 1105,215 | NaN

问题是Math.Pow sometimes returns a float that is not a number or NaN。具体来说:

x < 0 but not NegativeInfinity; y is not an integer, NegativeInfinity, or PositiveInfinity. = NaN

由于 Sin(x) 将绘制一条高于和低于 0 的曲线,因此它的某些值将为负,因此 Math.Pow 调用将产生 NaN,这当然不能在笛卡尔坐标系上绘制space.

我建议您暂时修改您的代码如下:用 try 块包装绘制调用。

try
{
    g.DrawLine(pen, x1 * eF, y1 * eF + yEx, x * eF, y2 * eF + yEx);
}
catch {}  //Not usually a great idea to eat all exceptions, but for troubleshooting purposes this'll do

然后 运行 您的代码并查看该行。换行符会告诉您 domain/range 的哪些部分有问题。一旦你对计算问题有了一个整体的看法,你就可以 transform/map 你的笛卡尔 space 进入一个不会发生溢出的域,例如通过将常数添加到 SIN 函数的结果。

好吧,原来解决方案是 TyCobb 的答案和 John Wu 的答案的混合。当您对 x 这样递增的值使用 Math.Sin 时,Math.Sin(x) 的大约一半输出将是负数。 Math.Pow 不喜欢你在第一个参数中给它一个负数而第二个参数给它一个 non-integer(在这种情况下它将 return NaN)。这样做的原因是因为当你将一个负数加 non-integer 时,结果是一个复数。

现在 Graphics.DrawLine 不关心你是否将 NaN 作为第二对 float 参数(x2, y2)中的任何一个传递,因为它只会 return什么都不做。但是,如果您将 NaN 作为第一对 (x1, y1) 中的一个传递,它将抛出 OverflowException。为什么会这样,我不确定,但我愿意猜测这是当时 .NET 团队的疏忽。

现在从第一个发现可以看出问题主要是在x为负的时候。对此的简单解决方案是将 x 替换为 Math.Pow(x),但这可能会以不反映原始函数意图的方式改变输出。第二种解决方案是简单地跳过这些迭代,但这会在图表中留下漏洞。

你可能已经猜到了第三种解决方案,那就是用Complex.Pow替换Math.Pow。这将为您提供潜在输入所需的支持,您可以通过 returning 一个复数获得幂函数。完成后,您可以选择要对结果执行的操作。 (我只是将结果的 Real 部分作为我的 y 并丢弃了 Imaginary 部分。)

for (float x = 0; x < Width; x += 0.005F)
{
    y2 = (float)Complex.Pow(Math.Sin(x), x).Real;

    g.DrawLine(pen, x1 * eF, y1 * eF + yEx, x * eF, y2 * eF + yEx);
    x1 = x;
    y1 = y2;
}

结果是这样的: