Silverlight数学引擎(4)——X之谜
在表达式中加入变量有什么用呢?看看下图就知道了:

含有x的表达式无法直接求值,需要先对x进行赋值,例如对于(y=x*x)不断的赋值、取值,可以绘制出一一列点(x,y),从而可以绘制出平滑的抛物线。
所以,XNode节点必须有个可赋值的属性:XValue,而且应该是静态的,以下是XNode的定义(超级简单,是不是?):
public class XNode : NodeBase { public static double XValue { get; set; } public XNode(int index, string data, string expression) : base(index, data, expression) { } public override double GetValue() { return XValue; } }
变量、常量的节点只要一次性解析完毕,以后的表达式重构中就不会再出现了,因此可以继承IExpressionAdjustor进行事先处理,处理完成后可以将XFinder从finders中移除以提高性能。
以下是XFinder的定义(现在你终于感觉到重构带来的优惠了吧!嘻嘻):
public class XFinder : FinderBase,IExpressionAdjustor { public override int Priority { get { return (int)FinderPriority.XFinder; } } protected override string Rule { get { return @"x"; } } protected override INode GenerateNode(string sourceExpression, string data, int index) { if (sourceExpression.Length > Match.Index + Match.Value.Length) { var tailChar = sourceExpression[Match.Index + Match.Value.Length]; //如果后面一个字符是':'或者是[a-zA-Z],则它是一个函数中的字符,不当做变量处理,否则当做变量 if (tailChar == '(' || (tailChar >= 'a' && tailChar <= 'z') || (tailChar >= 'A' && tailChar <= 'Z')) return null; } return new XNode(index, data, sourceExpression); } //重构原始表达式,并生成每个函数的Finder实例 public void AdjustExpression(ref string expression, ref List<IFinder> finders) { while (true) { INode node = Find(expression); if (node == null) break; AddNode(Calculator.FoundNodes, node); expression = expression.ReplaceOnce(node.Value, node.Id, node.Index); } finders = finders.Except(new List<IFinder> { this }).ToList(); //当前类的职责已经结束,将其移除 } }
由于含变量的表达式无法直接求值,Calculator中CalculateExpression方法也就不够用了,我们需要提供另外一个方法:GetValue(double x)以实现对变量的先赋值再取值。
以下是更新后的Calculator类,可以看出为了使用方便我们还提供了一个GetValues()的方法:
public class Calculator { private List<INode> _foundNodes; public List<INode> FoundNodes { get { return _foundNodes; } } public INode RootNode { get { return FoundNodes.Last(); } } public double CalculateExpression(string expression) { _foundNodes = new List<INode>(); FinderBase.FindAllNodes(this, ref expression); if (FoundNodes != null && FoundNodes.Count >= 1) return RootNode.GetValue(); return double.NaN; } public double GetValue(double x) { XNode.XValue = x; return RootNode.GetValue(); } //例如可以返回Points:(x1,y1),(x2,y2)... public List<Tuple<double, double>> GetValues(double xFrom, double xTo, int steps) { double oneStep = (xTo - xFrom)/steps; var rlt = new List<Tuple<double, double>>(); for (int i = 0; i < steps; i++) { XNode.XValue = xFrom + oneStep*i; RootNode.GetValue(); rlt.Add(new Tuple<double, double>(XNode.XValue, RootNode.GetValue())); } return rlt; } internal INode GetNode(string id) { return FoundNodes.FirstOrDefault(n => n.Id == id); } }
你可以注意到,第一次调用GetValue()方法之前必须先调用CalculateExpression()进行节点解析,此后就不需要进行解析了,因为直接使用解析出来的Nodes就可以了。
怎么展示X的美妙之处呢?对,画图演示,下面来说说平面直角坐标系:
数学上的平面直角坐标系与电脑的屏幕坐标系的差别不用说了吧,归纳一下是以下三点:
- Y轴方向相反。
- 原点位置(屏幕或者说UI控件如(Canvas)的坐标系原点在左上角顶点,数学坐标系通常在中心)
- 单位(屏幕坐标系通常以像素为单位,数学坐标系通常以单元(例如以1厘米为一个单元))
好了,有了这些差别,势必涉及到数学转换,例如屏幕上的位置(PhysicalPoint)转换为数学逻辑上的位置(LogicalPoint)。.net已经有了Point类型表示一个位置,我们可以直接用它,不过在这里为了避免混淆,我们还是先分别定义PhysicalPoint和LogicalPoint两个类吧,以免将来名不正言起来不顺啦:
public class LogicalPoint:PointBase { public LogicalPoint (double x,double y) : base(x,y){} public override PhysicalPoint ToPhysical(CoordinateSystem cs) { return cs.ToPhysical(this); } } public class PhysicalPoint : PointBase { public PhysicalPoint(double x, double y) : base(x,y){} public override LogicalPoint ToLogical(CoordinateSystem cs) { return cs.ToLogical(this); } }
转换必然会用到CoordinateSystem,就是我们定义的数学坐标系,因为我们可能会用到多个坐标系,而每个的单位长度可能不一样,以下是的CoordinateSystem定义,我们直接继承Canvas,因为坐标系通常要画坐标轴和刻度嘛,当然也可以采用聚合的方法,将Canvas作为CoordinateSystem的一个属性来实现。
public class CoordinateSystem:Canvas { public CoordinateSystem(double width,double height) { this.Width = width; this.Height = height; Origin = new PhysicalPoint(width/2, height/2); } private PhysicalPoint Origin; private const double UnitLength = 50;//每单位长度对应的屏幕像素 #region Coordinate transforms public LogicalPoint ToLogical(PhysicalPoint p) { return new LogicalPoint((p.X - Origin.X) / UnitLength, -(p.Y - Origin.Y) / UnitLength); } public PhysicalPoint ToPhysical(LogicalPoint p) { return new PhysicalPoint(Origin.X + p.X * UnitLength, Origin.Y - p.Y * UnitLength); } #endregion }
好了,至于在Canvas上画线啦、点啊什么的,不需要我多嘴了,Silverlight的特长,呵呵。不清楚的话可以直接看代码!
下面是运行截图,是不是比以前酷多了?

好啦,X之谜就是如此简单,但是为什么只有点,任然没有见到传说中的曲线呢?你可能已经猜到了:实现曲线还是有难度滴!如果我们直接将点与点用线段连起来,那对于像Sin(x)这样的表达式没问题,如果对于Sin(1/x)呢?或者更简单点直接对于y=1/x呢?悲剧了悲剧了!这涉及到求有效区间的问题,寡人暂时还没有想出来完全可行的方案,希望网友知道的话不吝赐教啊,在此谢过!
下一部分我们将对之前的所作所为做个总结,具体是加入动画演示来更形象的表述表达式解析和求值的过程,从而达到教学的目的!
浙公网安备 33010602011771号