代码改变世界

你正确的使用Out/Ref了吗?

2010-12-03 19:19  空逸云  阅读(721)  评论(6编辑  收藏  举报

最近在努力的学习MVC.并逐步的把Asp.net MVC应用到实际项目中去.先前.找了不少开源项目,心里一个高兴啊.一边摸索一边借鉴(抄)着别人的代码,并希望能从中吸收大牛们的思想.在CodePlex找到一个不错的开源项目.借鉴着就着手开发了.一路上风平浪静.抄代码抄得起劲.不料出现了一个让我措手不及的错误.该代码实现如下.

        /// <summary>
        /// 获取配置文件中DappSettings节点下指定索引键的Int类型的的值
        /// </summary>
        /// <param name="key">索引键</param>
        /// <param name="defaultValue">默认值</param>
        /// <returns>Int</returns>
        private static int getInt32(string key, int? defaultValue) {
            return getValue<int>(key, (v, pv) => int.TryParse(v, out pv), defaultValue);
        }
        private static T getValue<T>(string key, Func<string, T, bool> parseValue, T? defaultValue) where T : struct
        {
            string value = appSettings[key];
            if (value != null)
            {
                T parsedValue = default(T);

                if (parseValue(value, parsedValue))
                    return parsedValue;
                else
                    throw new ApplicationException(string.Format("Settings '{0}' was not a valid {1}", key, typeof(T).FullName));
            }
            if (!defaultValue.HasValue)
                throw new ApplicationException("在配置文件的appSettings节点结合中找不到key为" + key + "的子节点,且没有指定默认值");
            else
                return defaultValue.Value;
        }

该方法就是获取配置文件里相关的AppSetting项,并将其转换成Int32值.很不错的实现.但是实际上运行的时候却发现返回的值永远就是0,这不对啊.于是仔细的分析了这两个方法.主要就是利用了out关键字.

我们知道,利用Out/Ref关键字,能使值类型的参数达到引用类型的参数的效果.

瓦解黑盒子

仔细分析了这两个方法.将核心实现重现如下

    public class MyClass
    {
        static int parseValue(string str, int pv)
        {
            if (ValidParse(str, pv))
                return pv;
            else
                return 0;
        }

        static bool ValidParse(string str, int input)
        {
            return Int32.TryParse(str, out input);
        }


        public static void Main()
        {
            string str = "100";
            int num = 0;
            Console.WriteLine(parseValue(str, num));
        }

    }

看得到.调用步骤如下Main->parseValue->ValidParse,Main得到了一个字符串变量,想把它转换成Int类型,于是找到了parseValue方法.parseValue方法要求Main提供字符串和一个Int变量,它的做法是利用ValidParse方法,尝试能否吧str转换成Int,如果可以的话就返回Int,注意!此处的原意和TryParse相识.而ValidParse方法就是简单的调用了TryParse.大体看下来.这个流程是没什么问题的.而且应该得到正确的答案.但事实却并非如此.图解一下

image

可能图画得不太精确.但能诠释这个过程就好了.调用的过程.

第一步.看到.str是引用过去了.而num并不是原值,而是做了一个副本拷贝.这样.即使在parseValue中num的值发生改变.也只是副本数据发生了改变.

第二步.可以看到.str还是引用,即保持第一手数据.而num又做了一次副本拷贝.在validParse上调用了TryParse方法.此时,ValidParse上的num改变了.但是注意.此处也是副本修改.所以.

第三步(返回),num数据丢失.这也就解释了为什么我们的结果总会是0了.

既然知道了原因.那就好办了.我们尝试着加上out

    public class MyClass
    {
        static int parseValue(string str, int pv)
        {
            if (ValidParse(str, out pv))
                return pv;
            else
                return 0;
        }

        static bool ValidParse(string str, out int input)
        {
            return Int32.TryParse(str, out input);
        }


        public static void Main()
        {
            string str = "100";
            int num = 0;
            Console.WriteLine(parseValue(str, num));
        }

    }

改动并不大.只是在第二步中调用加入out,但是恰是这个out.让我们的结果正确了.原因就是第三步使用的num并不是步骤二的副本.而是步骤二的数据.所以它修改了原数据,没发生数据丢失.

优化代码

精简代码一向是我们所追崇的.特别是.Net3.5之后的Lambda.不用的话岂不浪费.废话不多说.我们开工

delegate bool ParseFunc<T, S>(T T1, out S S1); 

先声明一个委托,注意,这里的委托中用到了out.

        private static T getOutValue<T>(string key, ParseFunc<string,T> parseValue, T? defaultValue) where T : struct
        {
            string value = appSettings[key];
            if (value != null)
            {
                T parsedValue = default(T);

                if (parseValue(value, out parsedValue))
                    return parsedValue;
                else
                    throw new ApplicationException(string.Format("Settings '{0}' was not a valid {1}", key, typeof(T).FullName));
            }
            if (!defaultValue.HasValue)
                throw new ApplicationException("在配置文件的appSettings节点结合中找不到key为" + key + "的子节点,且没有指定默认值");
            else
                return defaultValue.Value;
        }

再改造一下GetValue方法.此处必须注意的是,在传参时必须使用Out,当然.你没用out,编译器也不会让你通过.最后

        public static int getInt32(string key, int? defaultValue)
        {
            return getOutValue<int>(key, (string v, out int pv) => int.TryParse(v, out pv), defaultValue);
        }

大功告成.这里主要是理解Lambda表达式的应用.相信你对Lambda的应用有了一个不一样的理解了吧!

PS:可能这个标题和文章内容有些格格不入,甚至有标题党的感觉.没什么文采.大家就体谅体谅.