警惕params object[]带来的歧义
最近在做Ext.Net的开发,在用到X.Call方法调用客户端的方法时遇到了这个问题,起初我以为是Ext.Net的bug,后来经过反复试验,发现这是一个陷阱,如果你没有遇到过此类问题,那么请下次见到params object[]时留一个心眼,如果你也曾遇到过,那就当加强一下记忆吧。
先让我来重现这个陷阱的过程:
1: <%@ Page Language="C#" %>
2:
3: <%@ Register Assembly="Ext.Net" Namespace="Ext.Net" TagPrefix="ext" %>
4: <script runat="server">
5: protected void Page_Load(object sender, EventArgs e)
6: {
7: X.Call("test", "hello", "world");
8: }
9: </script>
10: <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
11: <html runat="server" xmlns="http://www.w3.org/1999/xhtml">
12: <head id="Head1" runat="server">
13: <title></title>
14: <script type="text/javascript">
15: var test = function (arg1, arg2) {
16: alert(arg1 + ' ' + arg2);
17: }
18: </script>
19: </head>
20: <body>
21: <ext:ResourceManager runat="server" />
22: </body>
23: </html>
即使您没用过Ext.Net,我相信看了上面的代码也明白了X.Call的作用是在服务器端调用客户端的Javascript代码,这段代码将在网页上显示hello world,X.Call有两个重载:
1: public static void Call(string name);
2: public static void Call(string name, params object[] args);
第一个重载用于调用无参数的function,而第二个重载用于调用有参数的function,一切设计都很合理,也很方便,直到我用到了下面的代码:
1: <%@ Page Language="C#" %>
2:
3: <%@ Register Assembly="Ext.Net" Namespace="Ext.Net" TagPrefix="ext" %>
4: <script runat="server">
5: protected void Page_Load(object sender, EventArgs e)
6: {
7: var ids = new string[] { "001", "002", "003", "004" };
8: X.Call("showTrap", ids);
9: }
10: </script>
11: <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
12: <html runat="server" xmlns="http://www.w3.org/1999/xhtml">
13: <head id="Head1" runat="server">
14: <title></title>
15: <script type="text/javascript">
16: var showTrap = function (ids) {
17: for (var i = 0; i < ids.length; i++) {
18: //do something...
19: }
20: }
21: </script>
22: </head>
23: <body>
24: <ext:ResourceManager runat="server" />
25: </body>
26: </html>
很不幸,我中招了,我并没有在客户端收到预期的参数,客户端的ids里保存的是"001",也许你已经发现问题了,我原本的意图是将数组作为参数传递给客户端,可是客户端并没有生成我预期的代码,我预期的代码是:
1: showTrap(["001", "002", "003", "004"]);
而实际上生成的代码是:
1: showTrap("001", "002", "003", "004");
问题就出现在params object[]这个参数的解释上!string[]作为参数传递给params object[],并没有被解释成一个字符串数组参数,而是被解释成一组参数,每个参数都是一个字符串,也就是说string[]被当成了object[]!那是不是所有的Array都会被当成object[]呢?我们接着来测试一下,我们把string[]换成int[]:
1: <%@ Page Language="C#" %>
2:
3: <%@ Register Assembly="Ext.Net" Namespace="Ext.Net" TagPrefix="ext" %>
4: <script runat="server">
5: protected void Page_Load(object sender, EventArgs e)
6: {
7: var ids = new int[] { 1, 2, 3, 4 };
8: X.Call("showTrap", ids);
9: }
10: </script>
11: <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
12: <html runat="server" xmlns="http://www.w3.org/1999/xhtml">
13: <head id="Head1" runat="server">
14: <title></title>
15: <script type="text/javascript">
16: var showTrap = function (ids) {
17: for (var i = 0; i < ids.length; i++) {
18: //do something...
19: }
20: }
21: </script>
22: </head>
23: <body>
24: <ext:ResourceManager runat="server" />
25: </body>
26: </html>
生成的代码是什么呢?
1: showTrip([1, 2, 3, 4]);
竟然又正确了!这是为什么呢?相信读到这里你跟我一样都会把矛头指向值类型和引用类型,既然string[]是object[]那么int[]就不是object[]了吗?为了证实这点,我自己写了个类型来测试一下:
1: <%@ Page Language="C#" %>
2:
3: <%@ Register Assembly="Ext.Net" Namespace="Ext.Net" TagPrefix="ext" %>
4: <script runat="server">
5: public class TestRefType
6: {
7: private int i;
8:
9: public TestRefType(int i)
10: {
11: this.i = i;
12: }
13:
14: public override string ToString()
15: {
16: return i.ToString();
17: }
18: }
19: public struct TestValType
20: {
21: private int i;
22:
23: public TestValType(int i)
24: {
25: this.i = i;
26: }
27:
28: public override string ToString()
29: {
30: return i.ToString();
31: }
32: }
33: protected void Page_Load(object sender, EventArgs e)
34: {
35: var refArray = new TestRefType[] { new TestRefType(1), new TestRefType(2), new TestRefType(3), new TestRefType(4) };
36: X.Call("showTrap", refArray);
37: var valArray = new TestValType[] { new TestValType(1), new TestValType(2), new TestValType(3), new TestValType(4) };
38: X.Call("showTrap", valArray);
39: }
40: </script>
41: <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
42: <html id="Html1" runat="server" xmlns="http://www.w3.org/1999/xhtml">
43: <head id="Head1" runat="server">
44: <title></title>
45: <script type="text/javascript">
46: var showTrap = function (ids) {
47: alert(ids);
48: }
49: </script>
50: </head>
51: <body>
52: <ext:ResourceManager ID="ResourceManager1" runat="server" />
53: </body>
54: </html>
TestRefType和TestValType唯一的区别就是一个是class一个是struct,也就是引用类型和值类型的区别,当params object[]遇到这两个类型的数组时会如何解释呢?答案也许大家可以猜到了,TestRefType[]的行为与string[]的行为是一致的,TestValType[]与int[]的行为是一致的,下面阶段性小结一下:引用类型的数组被传入params object[]时会被解释为多个参数进行调用;值类型的数组被传入params object[]时会被解释为1个数组参数进行调用。
没有例外吗?让我们再深入的思考一下这个问题。
- 有没有办法打破这种编译器行为?
- 如果传入的不是Array而是List呢?
- 如果传入的就是IEnumerable呢?
先来看这段代码:
1: X.Call("showTrap", new string[] { "000", "001", "002", "003" }, null);
还是string[],但是我在后面加了一个参数null,这时第一个string[]将被当成一个参数来处理,这个很好理解,因为已经显示的传入了两个参数,所以每个参数都将被最为一个独立的参数看待,当然,我这样使用是不正确的,通常我们也不会这样在后面加一个null来使用,我只是为了说明这样的歧义只有在一个参数时才会存在。
再来看一段代码:
1: X.Call("showTrap", new List<string> { "000", "001", "002", "003" );
2: X.Call("showTrap", new string[] { "000", "001", "002", "003" }.AsEnumerable());
List<string>已经不是Array了,所以结果可想而知,List<string>将被作为一个独立的参数对待,那么IEnumerable呢?IEnumerable也是一样的,接口也不是数组,所以也将被作为一个独立的参数对待,这两个例子比较显而易见,看似多余,其实我是想说明另外一个要点:这样的歧义只有在参数是数组时才会存在。同时也给出了一个灵活的变通方案:当我们想把一个引用类型的数组单独的传入一个被设计为params object[]的参数时,最简单的做法是使用AsEnumerable转换成接口传入,我也是这样来“绕过”这个陷阱的,毕竟大多数情况下,我们需要的都不是数组类型的参数。所以,最后我想说的是,params这个关键字的设计初衷就是为了将多个同一类型的参数打包成数组来解决可变个数参数的调用的,比如最常见的String.Format方法,而直接传入数组调用并不多见,而本文中的陷阱更是实属罕见,并且这种陷阱仅仅存在于,对params object[]只传入一个数组类型的参数的场景。
希望本文对您有帮助。
转载请遵循此协议:署名 - 非商业用途 - 保持一致