画圆画方的故事

本文通过一个程序员的故事,介绍了如何使用双分派(Double Dispatch)解决面向对象设计中的多态问题,避免if-else和switch-case带来的代码臃肿。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

这个故事最初是来自和发哥的一次聊天,他说了一些面向对象设计方面挺有意思的事情,包括Double Dispatch(下面会提到),我根据我自己的体会和思考,把这些零散的片段重新整理成一个小故事,欢迎感兴趣的同学一起讨论。

 

有一个苦逼的程序员,叫做小P。

 

有一天,老板给他传达了这样一个需求,根据用户不同的图像绘制事件,画出一个圆或者是画出一个方块来。

老板传达的图像绘制事件是这样的:

 

 

interface DrawEvent {
}

class RoundDrawEvent implements DrawEvent {
}

class RectangleDrawEvent implements DrawEvent {
}
 

 

小P说,这个问题很简单:

 

public class Drawer {
	public void draw(DrawEvent event) {
		if (event instanceof RoundDrawEvent) {
			// 画圆
		} else if (event instanceof RectangleDrawEvent) {
			// 画方
		} else {
			System.out.print("error");
		}
	}
}

 

这似乎没有任何难度,一下就做出来了,不过,用户心情一好,就要提新的需求,而苦逼的程序员面对用户新的需求,是不能退缩的。用户说,现在我要求系统还要支持三角形。

 

小P想到,如果我再在代码里面增加一个if-else分支,问题是可以解决,可是分支越来越多,代码越来越丑陋,如果我可以用一个Map来代替if-else完成选择的功能,岂不是可以让我原来的实现优雅一点

 

public class Drawer {
	private static Map<Class, IDrawer> DRAWER_MAP = new HashMap<Class, IDrawer>();
	static {
		DRAWER_MAP.put(RoundDrawEvent.class, new RoundDrawer());
		DRAWER_MAP.put(RectangleDrawEvent.class, new RectangleDrawer());
	}

	public void draw(DrawEvent event) {
		DRAWER_MAP.get(event.getClass()).draw();
	}
}

interface IDrawer {
	public void draw();
}

class RoundDrawer implements IDrawer {
	public void draw() {
		// 画圆
	}
}

class RectangleDrawer implements IDrawer {
	public void draw() {
		// 画方
	}
}

 

突然,一瞬间的火花,小P觉得如果用方法重载来代替if-else的工作,把变化的点转移到方法重载上,也可以做到

 

 

public class Drawer {
	public void draw(RoundDrawEvent event) {
		//画圆
	}
	
	public void draw(RectangleDrawEvent event) {
		//画方
	}
	
	public void draw(DrawEvent event) {
		System.out.print("error");
	}
}

 

 

可以,测试这个方法的时候,他傻眼了:

 

 

DrawEvent event = new RoundDrawEvent();
new Drawer().draw(event);

 

 

他发现每次输出的结果都是“error”!

而如果测试代码改成这样,却是正确的:

 

 

new Drawer().draw(new RoundDrawEvent());

 

 

这是怎么回事?

 

看来小P和很数苦逼程序员还是有点不一样,他喜欢尝试、喜欢思考,而且还特别喜欢研究,一查到底。

原来,在Java中,方法重载都是在编译期间确定的,对于编译期间draw方法的实参event,如果使用了DrawEvent这个接口来引用,那么结果就可想而知,去执行draw(DrawEvent event)这个方法了。

 

原因清楚了,接下去就不难想出解决办法:

既然方法的重载无法是动态的,那么我在调用这个重载了的方法之前,我就要给它传入一个在编译期就已经确定了具体类型的入参,把变化的点转移到对象的多态上

可是,DrawEvent接口里并没有提供可供外部因素参与和影响的变化点,如果它能够提供一个供外部注入行为的变化点,不就可以用多态来帮助我们了么:

 

 

interface DrawEvent {
	public void draw(Drawer drawer);
}

class RoundDrawEvent implements DrawEvent {
	public void draw(Drawer drawer) {
		drawer.draw(this);
	}
}

class RectangleDrawEvent implements DrawEvent {
	public void draw(Drawer drawer) {
		drawer.draw(this);
	}
}

这里我说明一下为什么要传入drawer参数,因为真正要画图的家伙,不是这个event,而是drawer,而这个event只不过是利用多态,起到了寻找那个合适的重载方法的作用

 

好,接下去再完成Drawer就可以了:

 

 

public class Drawer {
	public void draw(RoundDrawEvent event) {
		// 画圆
	}

	public void draw(RectangleDrawEvent event) {
		// 画方
	}

	public void draw(DrawEvent event) {
		event.draw(this);
	}
}

可以看到,其实这里的DrawEvent已经不纯粹了,不仅仅代表了事件本身,还作为一个行为的委托者,甄选具体要执行的行为,再把执行的任务交还给Drawer

 

 

这时,我用下面的办法测试这个方法的时候,结果就是正确的了:

 

 

DrawEvent event = new RoundDrawEvent();
new Drawer().draw(event);

 

 

如果我把入参的引用变成具体类型,如:

 

 

new Drawer().draw(new RoundDrawEvent());

 

 

就直接走到Drawer的draw(RoundDrawEvent event)方法上了,于是结果也是正确的。

 

类似的实现方式,被称为Double Dispatch,它要根据两个对象的运行时类型来选择具体的执行方法(dispatches a function call to different concrete functions depending on the runtime types of two objects involved in the call)。

 

文章系本人原创,转载请注明出处和作者

 

### 如何在 Unity 中实现绘制曲线、形、矩形以及涂鸦功能 #### 使用的API 为了实现在Unity中绘制曲线、形、矩形及涂鸦的功能,主要依赖于`LineRenderer`组件来完成线条的绘制工作。此法适用于动态创建各种形状。 对于更复杂的图形如或椭,则可以通过计算一系列点的位置并通过这些点构建多边形近似表示。而矩形可以直接通过定义四个角点位置的式快速建立。至于自由手绘风格的涂鸦效果,通常会记录用户的触摸轨迹,并即时更新到场景之中[^1]。 下面给出具体操作指南: - **引入必要的命名空间** 需要使用`UnityEngine.LineRenderer`类来进行基本线条的描绘。 ```csharp using UnityEngine; ``` - **初始化 LineRenderer** 创建一个新的游戏对象并将`LineRenderer`附加上去作为子项。这一步骤可以在编辑器里手动执行也可编程自动化处理。 ```csharp GameObject lineObject = new GameObject("DynamicLine"); lineObject.AddComponent<LineRenderer>(); ``` - **配置 LineRenderer 属性** 设置宽度其他视觉特性以适应不同类型的笔触需求。 ```csharp LineRenderer lr = lineObject.GetComponent<LineRenderer>(); lr.startWidth = 0.1f; // 开始处线宽 lr.endWidth = 0.1f; // 结束处线宽 // 更多功能可查阅官文档进一步调整样式 ``` - **绘制简单直线/折线段** 向`LineRenderer`添加多个顶点形成连续路径即可构成复杂图案。 ```csharp Vector3[] points = {new Vector3(0,0,0), new Vector3(1,0,0)}; lr.positionCount = points.Length; lr.SetPositions(points); ``` - **绘制闭合图形 (例如矩形)** 对于封闭区域比如四边形,只需确保最后一个节点连接回起点形成闭环结构。 ```csharp Vector3[] rectPoints = { new Vector3(-0.5f,-0.5f,0), new Vector3(0.5f,-0.5f,0), new Vector3(0.5f,0.5f,0), new Vector3(-0.5f,0.5f,0), new Vector3(-0.5f,-0.5f,0) // 返回起始点关闭路径 }; lr.positionCount = rectPoints.Length; lr.SetPositions(rectPoints); ``` - **绘制平滑曲线** 若要生成更加流畅自然的效果,可以采用贝塞尔插值算法或其他数学函数生成中间过渡点。 ```csharp public static List<Vector3> GetBezierCurve(Vector3 p0, Vector3 p1, int resolution){ var curvePoints = new List<Vector3>(resolution); for(int i=0;i<=resolution;++i){ float t=i/(float)resolution; Vector3 pointOnCurve=Mathf.Pow((1-t),2)*p0+ Mathf.Sqrt(t*(1-t))*2*p1*t+ Mathf.Pow(t,2)*p1; curvePoints.Add(pointOnCurve); } return curvePoints; } ``` - **实现涂鸦板** 记录鼠标点击事件获取输入坐标序列,随后利用上述提到的技术实时呈现绘过程。 ```csharp void Update(){ if(Input.GetMouseButtonDown(0)){ StartNewStroke(); }else if(Input.GetMouseButton(0)){ AddPointToCurrentStroke(Input.mousePosition); } } private void StartNewStroke(){ // 初始化新的LineRenderer实例... } private void AddPointToCurrentStroke(Vector3 newPosition){ // 更新当前正在书写的LineRenderer数据... } ``` 以上就是在Unity环境下运用C#脚本配合内置工具集达成所需目标的一种式介绍。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值