C#模板编程(1):有了泛型,为什么还需要模板?

C#泛型编程已经深入人心了。为什么又提出C#模板编程呢?因为C#泛型存在一些局限性,突破这些局限性,需要使用C#方式的模板编程。由于C#语法、编译器、IDE限制,C#模板编程没有C++模板编程使用方便,但是,仍然可以解决一些问题。

 

下面先看C#泛型编程的两个限制:

(1)类型约束问题。

C#泛型的类型约束是个很严重的问题。

假设需要写一个泛型方法,这个方法有2个参数,然后方法返回结果是这两个参数的和。

这样的泛型方法无法直接实现。因为Byte,Int32等等并没有公共接口。

没有公共接口,但又想借助泛型来节省代码,减少维护量。怎么办呢?

我之前写过2篇博客《实现.net下的动态代理》、《实现.net下的动态代理(续)-多对象Mixin》,通过Ducking Typing来实现,但这种实现方式不自然,且性能低下,只适合对性能不敏感的场景。

(2)泛型指针问题。

泛型程序中无法使用泛型指针,因为编译器编译时无法知道具体类型的Size,这对写unsafe代码是很大的限制。

 

因此,我们需要C#模板编程。C#模板编程说起来很简单,它借助的是C#的一个语法——Using T0=T1。它没有C++那么自然,因为它缺乏C/C++源文件的Include机制。你可以将整块的文件在不同的类之间进行复制和粘帖。虽然复制和粘帖是一大邪恶,但总比复制/粘帖/替换要好很多。

下面是C#模板编程的一个重要应用——图像处理的空间线性滤波。关于空间线性滤波可参见各种图像处理的书,这里不做介绍。

我们假定图像是一个泛型类Image<T0>,这里T代表每一个像素的存储类型。可以是Byte,Short,Int32,Int64,Single,Double等。然后,核是一个泛型类,Kernel<T1>,这里还有第三个泛型类,就是用于存放中间数据的Image<T2>。为什么需要T2呢?假如T0是Byte,T1是带有Scale的Int32(如,Scale=9,每个元素值=1的3*3矩阵),计算的中间结果会超出Byte的最大值,需要一个不同类型缓存来存储这个中间数据。

只用泛型的话,解决不了这个问题。第一点,Byte,Short,Int32,Int64,Single,Double之间未实现共同接口;第二点,为提升性能,Image<T>采用非托管内存实现,使用指针进行操作,而泛型中无法使用泛型指针。

使用C#模板可以解决这个问题——代码照旧,只是在头部写下:Using T0=XXX;Using T1=XXX;Using T2=XXX;即可。

由于欠缺源代码的Include机制,当要编写新的滤波器时,需要把相同的代码复制过去,然后更改头部的Using ……。但无论如何,这样使用,还是比在代码中直接写明类型,然后复制、粘帖、替换新类型的可读性以及可维护性要好。

如果.Net允许源代码的Include,C#的模板编程将变得更为流畅(比起C++还是欠缺很多)。不知道等后续.Net版本开放编译服务之后,会不会有更优雅的写法。

下面是我随便写下的一段对图像进行空间线性滤波的代码(不要看具体算法,具体算法是极端错误的,且不完整的,只用看看编码风格就行了,写这段代码只为验证这种编程模式的优点和缺点):

using System;
using System.Collections.Generic;
using System.Text;

using T = System.Byte;
using CacheT = System.Int32;
using K = System.Int32;

namespace Orc.SmartImage.UnmanagedImage
{
    public static class FilterHelper
    {
        public unsafe static UnmanagedImage<T> Filter(this UnmanagedImage<T> src, FilterKernel<K> filter)
        {
            K* kernel = stackalloc K[filter.Length];

            Int32 srcWidth = src.Width;
            Int32 srcHeight = src.Height;
            Int32 kWidth = filter.Width;
            Int32 kHeight = filter.Height;

            T* start = (T*) src.StartIntPtr;
            T* lineStart = start;
            T* pStart = start;
            T* pTemStart = pStart;
            T* pT;
            Int32* pK;

            for (int c = 0; c < srcWidth; c++)
            {
                for (int r = 0; r < srcHeight; r++)
                {
                    pTemStart = pStart;
                    pK = kernel;

                    Int32 val = 0;
                    for (int kc = 0; kc < kWidth; kc++)
                    {
                        pT = pStart;
                        for (int kr = 0; kr < kHeight; kr++)
                        {
                            val += *pK * *pT;
                            pT++;
                            pK++;
                        }

                        pStart += srcWidth;
                    }

                    pStart = pTemStart;
                    pStart++;
                }

                lineStart += srcWidth;
                pStart = lineStart;
            }
            return null;
        }
    }
}

 

是不是很像模板编程呢?

若标题中有“转载”字样,则本文版权归原作者所有。若无转载字样,本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利.
posted @ 2010-03-22 16:22 xiaotie 阅读(3493) 评论(8) 编辑 收藏

 回复 引用 查看   
#1楼 2010-03-22 17:10 toEverybody      
一直让我郁闷的是,C#总想代替或接近C++的功能与性能,却绕了很大的圈子在尝试和应用C++所在作的事。。。,还不如直接把C++用IDE支持好

 回复 引用 查看   
#2楼[楼主] 2010-03-22 17:15 xiaotie      
@toEverybody
C++/CLI没C#/unsafe开发效率高

 回复 引用 查看   
#3楼 2010-03-22 18:00 传说哥      
没看到模板的影子,却闻到了它的味道~~
 回复 引用 查看   
#4楼 2010-03-22 18:24 地狱门神      
C#不支持约束(constraint),而接口又有很多限制,所以这里有这么多麻烦的东西。
F#貌似支持,不过F#的IDE支持现在还是太少了。

 回复 引用 查看   
#5楼 2010-03-22 18:41 陈梓瀚(vczh)      
不支持约束有一个很好的理由。假设你写了一个类A<T>,用反射可以生成很多实例A<int>啊A<string>什么的。支持约束的话,就只好把源代码带上了,就会跟C++一样了。
 回复 引用 查看   
#6楼[楼主] 2010-03-22 18:46 xiaotie      
@陈梓瀚(vczh)
给个开关,默认关上就行了。

 回复 引用 查看   
#7楼 2010-03-22 20:41 560889223      
混合语言编程。
把过多的特性强行塞入一种语言最终只会是悲剧,IMO。

 回复 引用 查看   
#8楼 2010-03-25 11:15 庄周梦蝶      
@560889223
引用560889223:
混合语言编程。
把过多的特性强行塞入一种语言最终只会是悲剧,IMO。


暂时支持!