为什么这个 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;
}
当您尝试绘制 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;
}
结果是这样的:
所以我制作了这个绘制正弦波的小 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;
}
当您尝试绘制 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;
}
结果是这样的: