0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
会员中心
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

C#模式匹配完全指南

新机器视觉 来源:hez2010 2023-09-13 17:33 次阅读

前言 自从 2017 年 C# 7.0 版本开始引入声明模式和常数模式匹配开始,到 2022 年的 C# 11 为止,最后一个板块列表模式和切片模式匹配也已经补齐,当初计划的模式匹配内容已经基本全部完成。 C# 在模式匹配方面下一步计划则是支持活动模式(active pattern),这一部分将在本文最后进行介绍,而在介绍未来的模式匹配计划之前,本文主题是对截止 C# 11 模式匹配的~~(不)~~完全指南,希望能对各位开发者们提升代码编写效率、可读性和质量有所帮助。 模式匹配 要使用模式匹配,首先要了解什么是模式。在使用正则表达式匹配字符串时,正则表达式自己就是一个模式,而对字符串使用这段正则表达式进行匹配的过程就是模式匹配。而在代码中也是同样的,我们对对象采用某种模式进行匹配的过程就是模式匹配。

C# 11 支持的模式有很多,包含:

  • 声明模式(declaration pattern)
  • 类型模式(type pattern)
  • 常数模式(constant pattern)
  • 关系模式(relational pattern)
  • 逻辑模式(logical pattern)
  • 属性模式(property pattern)
  • 位置模式(positional pattern)
  • var 模式(var pattern)
  • 丢弃模式(discard pattern)
  • 列表模式(list pattern)
  • 切片模式(slice pattern)

而其中,不少模式都支持递归,也就意味着可以模式嵌套模式,以此来实现更加强大的匹配功能。

模式匹配可以通过switch表达式来使用,也可以在普通的switch语句中作为case使用,还可以在if条件中通过is来使用。本文主要在switch表达式中使用模式匹配。

那么接下来就对这些模式进行介绍。

实例:表达式计算器

为了更直观地介绍模式匹配,我们接下来利用模式匹配来编写一个表达式计算器。

为了编写表达式计算器,首先我们需要对表达式进行抽象:

publicabstractpartialclassExpr<T>whereT:IBinaryNumber<T>
{
publicabstractTEval(params(stringName,TValue)[]args);
}

我们用上面这个Expr来表示一个表达式,其中T是操作数的类型,然后进一步将表达式分为常数表达式ConstantExpr参数表达式ParameterExpr、一元表达式UnaryExpr、二元表达式BinaryExpr和三元表达式TernaryExpr。最后提供一个Eval方法,用来计算表达式的值,该方法可以传入一个args来提供表达式计算所需要的参数。

有了一、二元表达式自然也需要运算符,例如加减乘除等,我们也同时定义Operator来表示运算符:

publicabstractrecordOperator
{
publicrecordUnaryOperator(OperatorsOperator):Operator;
publicrecordBinaryOperator(BinaryOperatorsOperator):Operator;
}

然后设置允许的运算符,其中前三个是一元运算符,后面的是二元运算符:

publicenumOperators
{
[Description("~")]Inv,[Description("-")]Min,[Description("!")]LogicalNot,
[Description("+")]Add,[Description("-")]Sub,[Description("*")]Mul,[Description("/")]Div,
[Description("&")]And,[Description("|")]Or,[Description("^")]Xor,
[Description("==")]Eq,[Description("!=")]Ne,
[Description(">")]Gt,[Description("<")]Lt,[Description(">=")]Ge,[Description("<=")]Le,
[Description("&&")]LogicalAnd,[Description("||")]LogicalOr,
}

你可以能会好奇对T的运算能如何实现逻辑与或非,关于这一点,我们直接使用0来代表false,非0代表true

接下来就是分别实现各类表达式的时间!

常数表达式

常数表达式很简单,它保存一个常数值,因此只需要在构造方法中将用户提供的值存储下来。它的Eval实现也只需要简单返回存储的值即可:

publicabstractpartialclassExpr<T>whereT:IBinaryNumber<T>
{
publicclassConstantExpr:Expr<T>
{
publicConstantExpr(Tvalue)=>Value=value;

publicTValue{get;}
publicvoidDeconstruct(outTvalue)=>value=Value;

publicoverrideTEval(params(stringName,TValue)[]args)=>Value;
}
}

参数表达式

参数表达式用来定义表达式计算过程中的参数,允许用户在对表达式执行Eval计算结果的时候传参,因此只需要存储参数名。它的Eval实现需要根据参数名在args中找出对应的参数值:

publicabstractpartialclassExpr<T>whereT:IBinaryNumber<T>
{
publicclassParameterExpr:Expr<T>
{
publicParameterExpr(stringname)=>Name=name;

publicstringName{get;}
publicvoidDeconstruct(outstringname)=>name=Name;

//对args进行模式匹配
publicoverrideTEval(params(stringName,TValue)[]args)=>argsswitch
{
//如果args有至少一个元素,那我们把第一个元素拿出来存为(name,value),
//然后判断 name 是否和本参数表达式中存储的参数名 Name 相同。
//如果相同则返回 value,否则用 args 除去第一个元素剩下的参数继续匹配。
[var(name,value),..vartail]=>name==Name?value:Eval(tail),
//如果args是空列表,则说明在args中没有找到名字和Name相同的参数,抛出异常
[]=>thrownewInvalidOperationException($"Expectedanargumentnamed{Name}.")
};
}
}

模式匹配会从上往下依次进行匹配,直到匹配成功为止。

上面的代码中你可能会好奇[var (name, value), .. var tail]是个什么模式,这个模式整体看是列表模式,并且列表模式内组合使用声明模式、位置模式和切片模式。例如:

  • []:匹配一个空列表。
  • [1, _, 3]:匹配一个长度是 3,并且首尾元素分别是 1、3 的列表。其中_是丢弃模式,表示任意元素。
  • [_, .., 3]:匹配一个末元素是 3,并且 3 不是首元素的列表。其中..是切片模式,表示任意切片。
  • [1, ..var tail]:匹配一个首元素是 1 的列表,并且将除了首元素之外元素的切片赋值给tail。其中var tailvar模式,用于将匹配结果赋值给变量。
  • [var head, ..var tail]:匹配一个列表,将它第一个元素赋值给head,剩下元素的切片赋值给tail,这个切片里可以没有元素。
  • [var (name, value), ..var tail]:匹配一个列表,将它第一个元素赋值给(name, value),剩下元素的切片赋值给tail,这个切片里可以没有元素。其中(name, value)是位置模式,用于将第一个元素的解构结果根据位置分别赋值给namevalue,也可以写成(var name, var value)

一元表达式

一元表达式用来处理只有一个操作数的计算,例如非、取反等。

publicabstractpartialclassExpr<T>whereT:IBinaryNumber<T>
{
publicclassUnaryExpr:Expr<T>
{
publicUnaryExpr(UnaryOperatorop,Exprexpr)=>(Op,Expr)=(op,expr);

publicUnaryOperatorOp{get;}
publicExprExpr{get;}
publicvoidDeconstruct(outUnaryOperatorop,outExprexpr)=>(op,expr)=(Op,Expr);

//对Op进行模式匹配
publicoverrideTEval(params(stringName,TValue)[]args)=>Opswitch
{
//如果Op是UnaryOperator,则将其解构结果赋值给op,然后对op进行匹配,op是一个枚举,而.NET中的枚举值都是整数
UnaryOperator(varop)=>opswitch
{
//如果op是Operators.Inv
Operators.Inv=>~Expr.Eval(args),
//如果op是Operators.Min
Operators.Min=>-Expr.Eval(args),
//如果op是Operators.LogicalNot
Operators.LogicalNot=>Expr.Eval(args)==T.Zero?T.One:T.Zero,
//如果op的值大于LogicalNot或者小于0,表示不是一元运算符
>Operators.LogicalNotor< 0=>thrownewInvalidOperationException($"Expectedanunaryoperator,butgot{op}.")
},
//如果Op不是UnaryOperator
_=>thrownewInvalidOperationException("Expectedanunaryoperator.")
};
}
}

上面的代码中,首先利用了 C# 元组可作为左值的特性,分别使用一行代码就做完了构造方法和解构方法的赋值:(Op, Expr) = (op, expr)(op, expr) = (Op, Expr)。如果你好奇能否利用这个特性交换多个变量,答案是可以!

Eval中,首先将类型模式、位置模式和声明模式组合成UnaryOperator(var op),表示匹配UnaryOperator类型、并且能解构出一个元素的东西,如果匹配则将解构出来的那个元素赋值给op

然后我们接着对解构出来的op进行匹配,这里用到了常数模式,例如Operators.Inv用来匹配op是否是Operators.Inv。常数模式可以使用各种常数对对象进行匹配。

这里的> Operators.LogicalNot< 0则是关系模式,分别用于匹配大于Operators.LogicalNot的值和小于0的指。然后利用逻辑模式or将两个模式组合起来表示或的关系。逻辑模式除了or之外还有andnot

由于我们在上面穷举了枚举中所有的一元运算符,因此也可以将> Operators.LogicalNot or < 0换成丢弃模式_或者 var 模式var foo,两者都用来匹配任意的东西,只不过前者匹配到后直接丢弃,而后者声明了个变量foo将匹配到的值放到里面:

opswitch
{
//...
_=>thrownewInvalidOperationException($"Expectedanunaryoperator,butgot{op}.")
}

opswitch
{
//...
varfoo=>thrownewInvalidOperationException($"Expectedanunaryoperator,butgot{foo}.")
}

二元表达式

二元表达式用来表示操作数有两个的表达式。有了一元表达式的编写经验,二元表达式如法炮制即可。

publicabstractpartialclassExpr<T>whereT:IBinaryNumber<T>
{
publicclassBinaryExpr:Expr<T>
{
publicBinaryExpr(BinaryOperatorop,Exprleft,Exprright)=>(Op,Left,Right)=(op,left,right);

publicBinaryOperatorOp{get;}
publicExprLeft{get;}
publicExprRight{get;}
publicvoidDeconstruct(outBinaryOperatorop,outExprleft,outExprright)=>(op,left,right)=(Op,Left,Right);

publicoverrideTEval(params(stringName,TValue)[]args)=>Opswitch
{
BinaryOperator(varop)=>opswitch
{
Operators.Add=>Left.Eval(args)+Right.Eval(args),
Operators.Sub=>Left.Eval(args)-Right.Eval(args),
Operators.Mul=>Left.Eval(args)*Right.Eval(args),
Operators.Div=>Left.Eval(args)/Right.Eval(args),
Operators.And=>Left.Eval(args)&Right.Eval(args),
Operators.Or=>Left.Eval(args)|Right.Eval(args),
Operators.Xor=>Left.Eval(args)^Right.Eval(args),
Operators.Eq=>Left.Eval(args)==Right.Eval(args)?T.One:T.Zero,
Operators.Ne=>Left.Eval(args)!=Right.Eval(args)?T.One:T.Zero,
Operators.Gt=>Left.Eval(args)>Right.Eval(args)?T.One:T.Zero,
Operators.Lt=>Left.Eval(args)< Right.Eval(args) ? T.One : T.Zero,
                Operators.Ge =>Left.Eval(args)>=Right.Eval(args)?T.One:T.Zero,
Operators.Le=>Left.Eval(args)<= Right.Eval(args) ? T.One : T.Zero,
                Operators.LogicalAnd =>Left.Eval(args)==T.Zero||Right.Eval(args)==T.Zero?T.Zero:T.One,
Operators.LogicalOr=>Left.Eval(args)==T.Zero&&Right.Eval(args)==T.Zero?T.Zero:T.One,
< Operators.Add or >Operators.LogicalOr=>thrownewInvalidOperationException($"Unexpectedabinaryoperator,butgot{op}.")
},
_=>thrownewInvalidOperationException("Unexpectedabinaryoperator.")
};
}
}

同理,也可以将< Operators.Add or > Operators.LogicalOr换成丢弃模式或者 var 模式。

三元表达式

三元表达式包含三个操作数:条件表达式Cond、为真的表达式Left、为假的表达式Right。该表达式中会根据Cond是否为真来选择取Left还是Right,实现起来较为简单:

publicabstractpartialclassExpr<T>whereT:IBinaryNumber<T>
{
publicclassTernaryExpr:Expr<T>
{
publicTernaryExpr(Exprcond,Exprleft,Exprright)=>(Cond,Left,Right)=(cond,left,right);

publicExprCond{get;}
publicExprLeft{get;}
publicExprRight{get;}
publicvoidDeconstruct(outExprcond,outExprleft,outExprright)=>(cond,left,right)=(Cond,Left,Right);

publicoverrideTEval(params(stringName,TValue)[]args)=>Cond.Eval(args)==T.Zero?Right.Eval(args):Left.Eval(args);
}
}

完成。我们用了仅仅几十行代码就完成了全部的核心逻辑!这便是模式匹配的强大之处:简洁、直观且高效。

表达式判等

至此为止,我们已经完成了所有的表达式构造、解构和计算的实现。接下来我们为每一个表达式实现判等逻辑,即判断两个表达式(字面上)是否相同。

例如a == b ? 2 : 4a == b ? 2 : 5不相同,a == b ? 2 : 4c == d ? 2 : 4不相同,而a == b ? 2 : 4a == b ? 2 : 4相同。

为了实现该功能,我们重写每一个表达式的EqualsGetHashCode方法。

常数表达式

常数表达式判等只需要判断常数值是否相等即可:

publicoverrideboolEquals(object?obj)=>objisConstantExpr(varvalue)&&value==Value;
publicoverrideintGetHashCode()=>Value.GetHashCode();

参数表达式

参数表达式判等只需要判断参数名是否相等即可:

publicoverrideboolEquals(object?obj)=>objisParameterExpr(varname)&&name==Name;
publicoverrideintGetHashCode()=>Name.GetHashCode();

一元表达式

一元表达式判等,需要判断被比较的表达式是否是一元表达式,如果也是的话则判断运算符和操作数是否相等:

publicoverrideboolEquals(object?obj)=>objisUnaryExpr({Operator:varop},varexpr)&&(op,expr).Equals((Op.Operator,Expr));
publicoverrideintGetHashCode()=>(Op,Expr).GetHashCode();

上面的代码中用到了属性模式{ Operator: var op },用来匹配属性的值,这里直接组合了声明模式将属性Operator的值赋值给了expr。另外,C# 中的元组可以组合起来进行判等操作,因此不需要写op.Equals(Op.Operator) && expr.Equals(Expr),而是可以直接写(op, expr).Equals((Op.Operator, Expr))

二元表达式

和一元表达式差不多,区别在于这次多了一个操作数:

publicoverrideboolEquals(object?obj)=>objisBinaryExpr({Operator:varop},varleft,varright)&&(op,left,right).Equals((Op.Operator,Left,Right));
publicoverrideintGetHashCode()=>(Op,Left,Right).GetHashCode();

三元表达式

和二元表达式差不多,只不过运算符Op变成了操作数Cond

publicoverrideboolEquals(object?obj)=>objisTernaryExpr(varcond,varleft,varright)&&cond.Equals(Cond)&&left.Equals(Left)&&right.Equals(Right);
publicoverrideintGetHashCode()=>(Cond,Left,Right).GetHashCode();

到此为止,我们为所有的表达式都实现了判等。

一些工具方法

我们重载一些Expr的运算符方便我们使用:

publicstaticExproperator~(Exproperand)=>newUnaryExpr(new(Operators.Inv),operand);
publicstaticExproperator!(Exproperand)=>newUnaryExpr(new(Operators.LogicalNot),operand);
publicstaticExproperator-(Exproperand)=>newUnaryExpr(new(Operators.Min),operand);
publicstaticExproperator+(Exprleft,Exprright)=>newBinaryExpr(new(Operators.Add),left,right);
publicstaticExproperator-(Exprleft,Exprright)=>newBinaryExpr(new(Operators.Sub),left,right);
publicstaticExproperator*(Exprleft,Exprright)=>newBinaryExpr(new(Operators.Mul),left,right);
publicstaticExproperator/(Exprleft,Exprright)=>newBinaryExpr(new(Operators.Div),left,right);
publicstaticExproperator&(Exprleft,Exprright)=>newBinaryExpr(new(Operators.And),left,right);
publicstaticExproperator|(Exprleft,Exprright)=>newBinaryExpr(new(Operators.Or),left,right);
publicstaticExproperator^(Exprleft,Exprright)=>newBinaryExpr(new(Operators.Xor),left,right);
publicstaticExproperator>(Exprleft,Exprright)=>newBinaryExpr(new(Operators.Gt),left,right);
publicstaticExproperator<(Exprleft,Exprright)=>newBinaryExpr(new(Operators.Lt),left,right);
publicstaticExproperator>=(Exprleft,Exprright)=>newBinaryExpr(new(Operators.Ge),left,right);
publicstaticExproperator<=(Exprleft,Exprright)=>newBinaryExpr(new(Operators.Le),left,right);
publicstaticExproperator==(Exprleft,Exprright)=>newBinaryExpr(new(Operators.Eq),left,right);
publicstaticExproperator!=(Exprleft,Exprright)=>newBinaryExpr(new(Operators.Ne),left,right);
publicstaticimplicitoperatorExpr(Tvalue)=>newConstantExpr(value);
publicstaticimplicitoperatorExpr(stringname)=>newParameterExpr(name);
publicstaticimplicitoperatorExpr(boolvalue)=>newConstantExpr(value?T.One:T.Zero);

publicoverrideboolEquals(object?obj)=>base.Equals(obj);
publicoverrideintGetHashCode()=>base.GetHashCode();

由于重载了==!=,编译器为了保险起见提示我们重写EqualsGetHashCode,这里实际上并不需要重写,因此直接调用base上的方法保持默认行为即可。

然后编写两个扩展方法用来方便构造三元表达式,和从Description中获取运算符的名字:

publicstaticclassExtensions
{
publicstaticExprSwitch(thisExprcond,Exprleft,Exprright)whereT:IBinaryNumber=>newExpr.TernaryExpr(cond,left,right);
publicstaticstring?GetName(thisTop)whereT:Enum=>typeof(T).GetMember(op.ToString()).FirstOrDefault()?.GetCustomAttribute()?.Description;
}

由于有参数表达式参与时需要我们提前提供参数值才能调用Eval进行计算,因此我们写一个交互式的Eval来在计算过程中遇到参数表达式时提示用户输入值,起名叫做InteractiveEval

publicTInteractiveEval()
{
varnames=Array.Empty<string>();
returnEval(GetArgs(this,refnames,refnames));
}
privatestaticTGetArg(stringname,refstring[]names)
{
Console.Write($"Parameter{name}:");
string?str;
do{str=Console.ReadLine();}
while(strisnull);
names=names.Append(name).ToArray();
returnT.Parse(str,NumberStyles.Number,null);
}
privatestatic(stringName,TValue)[]GetArgs(Exprexpr,refstring[]assigned,refstring[]filter)=>exprswitch
{
TernaryExpr(varcond,varleft,varright)=>GetArgs(cond,refassigned,refassigned).Concat(GetArgs(left,refassigned,refassigned)).Concat(GetArgs(right,refassigned,refassigned)).ToArray(),
BinaryExpr(_,varleft,varright)=>GetArgs(left,refassigned,refassigned).Concat(GetArgs(right,refassigned,refassigned)).ToArray(),
UnaryExpr(_,varuexpr)=>GetArgs(uexpr,refassigned,refassigned),
ParameterExpr(varname)=>filterswitch
{
[varhead,..]whenhead==name=>Array.Empty<(stringName,TValue)>(),
[_,..vartail]=>GetArgs(expr,refassigned,reftail),
[]=>new[]{(name,GetArg(name,refassigned))}
},
_=>Array.Empty<(stringName,TValue)>()
};

这里在GetArgs方法中,模式[var head, ..]后面跟了一个when head == name,这里的when用来给模式匹配指定额外的条件,仅当条件满足时才匹配成功,因此[var head, ..] when head == name的含义是,匹配至少含有一个元素的列表,并且将头元素赋值给head,且仅当head == name时匹配才算成功。

最后我们再重写ToString方法方便输出表达式,就全部大功告成了。

测试

接下来让我测试测试我们编写的表达式计算器:

Expr<int>a=4;
Expr<int>b=-3;
Expr<int>x="x";
Expr<int>c=!((a+b)*(a-b)>x);
Expr<int>y="y";
Expr<int>z="z";
Expr<int>expr=(c.Switch(y,z)-a>x).Switch(z+a,y/b);
Console.WriteLine(expr);
Console.WriteLine(expr.InteractiveEval());

运行后得到输出:

((((! ((((4) + (-3)) * ((4) - (-3))) > (x))) ? (y) : (z)) - (4)) > (x)) ? ((z) + (4)) : ((y) / (-3))

然后我们给xyz分别设置成 42、27 和 35,即可得到运算结果:

Parameterx:42
Parametery:27
Parameterz:35
-9

再测测表达式判等逻辑:

Expr<int>expr1,expr2,expr3;
{
Expr<int>a=4;
Expr<int>b=-3;
Expr<int>x="x";
Expr<int>c=!((a+b)*(a-b)>x);
Expr<int>y="y";
Expr<int>z="z";
expr1=(c.Switch(y,z)-a>x).Switch(z+a,y/b);
}

{
Expr<int>a=4;
Expr<int>b=-3;
Expr<int>x="x";
Expr<int>c=!((a+b)*(a-b)>x);
Expr<int>y="y";
Expr<int>z="z";
expr2=(c.Switch(y,z)-a>x).Switch(z+a,y/b);
}

{
Expr<int>a=4;
Expr<int>b=-3;
Expr<int>x="x";
Expr<int>c=!((a+b)*(a-b)>x);
Expr<int>y="y";
Expr<int>w="w";
expr3=(c.Switch(y,w)-a>x).Switch(w+a,y/b);
}

Console.WriteLine(expr1.Equals(expr2));
Console.WriteLine(expr1.Equals(expr3));

得到输出:

True
False

活动模式

在未来,C# 将会引入活动模式,该模式允许用户自定义模式匹配的方法,例如:

staticboolEven(thisTvalue)whereT:IBinaryInteger=>value%2==0;

上述代码定义了一个T的扩展方法Even,用来匹配value是否为偶数,于是我们便可以这么使用:

varx=3;
vary=xswitch
{
Even()=>"even",
_=>"odd"
};

此外,该模式还可以和解构模式结合,允许用户自定义解构行为,例如:

staticboolInt(thisstringvalue,outintresult)=>int.TryParse(value,outresult);

然后使用的时候:

varx="3";
vary=xswitch
{
Int(varresult)=>result,
_=>0
};

即可对x这个字符串进行匹配,如果x可以被解析为int,就取解析结果result,否则取 0。

总结

模式匹配极大的方便了我们编写出简洁且可读性高的高质量代码,并且会自动帮我们做穷举检查,防止我们漏掉情况。此外,使用模式匹配时,编译器也会帮我们优化代码,减少完成匹配所需要的比较次数,最终减少分支并提升运行效率。

本文中的例子为了覆盖到全部的模式,不一定采用了最优的写法,这一点各位读者们也请注意。

本文中的表达式计算器全部代码可以前往


声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • 字符串
    +关注

    关注

    1

    文章

    579

    浏览量

    20512
  • 代码
    +关注

    关注

    30

    文章

    4787

    浏览量

    68569
  • 模式
    +关注

    关注

    0

    文章

    65

    浏览量

    13384

原文标题:C# 模式匹配完全指南

文章出处:【微信号:vision263com,微信公众号:新机器视觉】欢迎添加关注!文章转载请注明出处。

收藏 人收藏

    评论

    相关推荐

    C#模式匹配入门指南

    自从 2017 年 C# 7.0 版本开始引入声明模式和常数模式匹配开始,到 2022 年的 C# 11 为止,最后一个板块列表
    的头像 发表于 09-18 09:36 705次阅读

    C#上位机显示不完全

    我用原子的板子和C#上位机通讯。上位机显示不完全。但是用网络助手通讯的话,显示是完全的。所以应该是我C#上位机的问题。大家帮忙看看,是什么问题。以下 是代码CSharpTCP.rar
    发表于 04-02 23:26

    C#完全手册

    C#语言概述点NET编程语言C#运行环境编写第一个程序C#程序设计基础数据类型变量和和常量......
    发表于 05-21 22:00 144次下载

    C#教程之DisplayRowCount

    C#教程之DisplayRowCount,很好的C#资料,快来学习吧。
    发表于 04-20 09:59 8次下载

    C#教程之FileBatchCopy

    C#教程之FileBatchCopy,很好的C#资料,快来学习吧。
    发表于 04-20 09:59 12次下载

    C#教程之弹出模式窗口显示进度条

    C#教程之弹出模式窗口显示进度条,很好的C#资料,快来学习吧。
    发表于 04-20 10:49 7次下载

    C#教程之组合

    C#教程之组合,很好的C#资料,快来学习吧。
    发表于 04-20 10:50 15次下载

    C#教程之LoadFLASH

    C#教程之LoadFLASH,很好的C#资料,快来学习吧。
    发表于 04-20 10:50 9次下载

    C#教程之PrintRemitBill

    C#教程之PrintRemitBill,很好的C#资料,快来学习吧。
    发表于 04-20 13:50 16次下载

    C#教程之PrintTopFive

    C#教程之PrintTopFive,很好的C#资料,快来学习吧。
    发表于 04-20 13:50 5次下载

    C#教程之VPrintt

    C#教程之VPrintt,很好的C#资料,快来学习吧。
    发表于 04-20 14:06 10次下载

    C#教程之WordToHtml

    C#教程之WordToHtml,很好的C#资料,快来学习吧。
    发表于 04-20 14:46 15次下载

    《Visual C# 2005开发技术》C#与.NET Fram

    《Visual C# 2005开发技术》C#与.NET Framework简介
    发表于 02-07 15:11 0次下载

    《Visual C# 2005开发技术》C#程序设计基础

    《Visual C# 2005开发技术》C#程序设计基础
    发表于 02-07 15:11 0次下载

    C#上位机实战开发指南

    C#上位机实战开发指南
    发表于 11-22 19:25 0次下载