通过 WCF 认识 F# 中的类型

近来正在试着用 F# 实现自承载 WCF 服务, 供 Silverlight 调用.一些数据会通过 “添加服务引用” 这一功能, 实现从 F# 到 C# 的转换. 正好借此机会看下会发生什么样有趣的事情.

由于我要在服务端读取数据库, 之前的 LINQ to SQL 经历告诉我, 我应该为数据库的每个表定义相应的类, 以 Boy 表为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
(* F# 在类(Class)的定义方面是很灵活的从某种意义上讲 对类的表达近乎恶搞
因为类(Class)对 F# 来说只不过是个表达式而已 *)


// 可以这样:
type Boy1 =
val mutable _name :string
val mutable _age :int
member t.Name
with get() = t._name
and set(v) = t._name <- v
member t.Age
with get() = t._age
and set(v) = t._age <- v
new () =
{
_name = ""
_age = 0
}
// 这种形式要求必须实例化之后才能使用
let boya =
let boy = new Boy1()
boy._name <- "colder"
boy._age <- Int32.MaxValue
boy
// 或
let boyb = new Boy1(Name = "colder", Age = Int32.MaxValue)

// 还可以定义成这样:
type Boy2(name :string, age :int) =
let mutable _name = name
let mutable _age = age
member t.Name
with get() = _name
and set(v) = _name <- v
member t.Age
with get() = _age
and set(v) = _age <- v
// 实例化时参数即为字段
let boyc = new Boy2("colder", Int32.MaxValue)

// 这两种方式定义的类 都是在模仿 C# 定义类的方式
// 不过怎么看都没有 C#3.0 使用的 { get; set; } 形式简单
// 可不可以使用 Records 呢?
// 答案是肯定的 生成的客户端代码并没有区别
type Boy =
{
mutable Name :string
mutable Age :int
}
// 实例化也是最简单的
let boyd =
{
Name = "colder"
Age = Int32.MaxValue
}

验证的依据很简单, 在服务端开放数据契约和服务契约, 之后生成客户端代码, 看是否有异常就可以了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 给 Boy 记录加上相关的属性
[<DataContract(Name="Boy")>]
type Boy =
{
[<DataMember(Order = 1)>]
mutable Name :string
[<DataMember(Order = 2)>]
mutable Age :int
}
// 返回值采用标准的 List<T> 泛型
// 也就是微软官方视频教程中提到的 LINQ 表达式的 ToList() 方法返回的类型
[<ServiceContract(Name = "Wcf")>]
type Wcf() =
let boy =
{
Name = "colder"
Age = Int32.MaxValue
}
[<OperationContract>]
member t.BoyListT() =
let list = new System.Collections.Generic.List<Boy>()
list.Add boy
list

使用 WCF 工具生成客户端代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
// 以下是客户端服务引用自动生成的代码片段
[System.Diagnostics.DebuggerStepThroughAttribute()]
[System.Runtime.Serialization.DataContractAttribute(Name="Boy", Namespace="http://schemas.datacontract.org/2004/07/")]
public partial class Boy : object, System.ComponentModel.INotifyPropertyChanged
{
private string NameField;
private int AgeField;
[System.Runtime.Serialization.DataMemberAttribute()]
public string Name
{
get
{
return this.NameField;
}
set
{
if ((object.ReferenceEquals(this.NameField, value) != true))
{
this.NameField = value;
this.RaisePropertyChanged("Name");
}
}
}
[System.Runtime.Serialization.DataMemberAttribute(Order=1)]
public int Age
{
get
{
return this.AgeField;
}
set
{
if ((this.AgeField.Equals(value) != true))
{
this.AgeField = value;
this.RaisePropertyChanged("Age");
}
}
}
public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged;
protected void RaisePropertyChanged(string propertyName)
{
System.ComponentModel.PropertyChangedEventHandler propertyChanged = this.PropertyChanged;
if ((propertyChanged != null))
{
propertyChanged(this, new System.ComponentModel.PropertyChangedEventArgs(propertyName));
}
}
}

上面这个例子生动的说明: 什么时候该使用 Class, 什么时候该使用 Records.个人理解, 有行为的才是 Class, 只是数据的就应该是 Records.

难怪蔡学镛先生会提醒初学 F# 的人 宁可矫枉过正 也要保持 FP 的原汁原味 只有这样才能深刻体会 FP 的真谛

数据契约弄清楚了, 那 WCF 操作契约的返回值可不可以采用 F# 常见的 list 或 seq 之类的呢? 试一下便清楚了!

1
2
3
4
5
6
7
8
9
10
11
12
// 修改契约 返回 list, 也就是 F# 特有的数据类型 Boy list.
// seq 会返回同样的效果, 不再重复了.
[<ServiceContract(Name = "Wcf")>]
type Wcf() =
let boy =
{
Name = "colder"
Age = Int32.MaxValue
}
[<OperationContract>]
member t.BoyList() =
[boy]

这次客户端代码有点奇怪了.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 生成的 C# 代码是个畸型的怪胎...
// 不可思议的类名: ListOfBoyMTRdQN6P
[System.Diagnostics.DebuggerStepThroughAttribute()]
[System.Runtime.Serialization.DataContractAttribute(Name="ListOfBoyMTRdQN6P", Namespace="http://schemas.datacontract.org/2004/07/Microsoft.FSharp.Collections")]
public partial class ListOfBoyMTRdQN6P : object, System.ComponentModel.INotifyPropertyChanged
{
public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged;
protected void RaisePropertyChanged(string propertyName)
{
System.ComponentModel.PropertyChangedEventHandler propertyChanged = this.PropertyChanged;
if ((propertyChanged != null))
{
propertyChanged(this, new System.ComponentModel.PropertyChangedEventArgs(propertyName));
}
}
}

看到了吧, 此 list 非彼 List, 真的想不通把 Boy 放到 list 以后究竟发生了什么, 导致离开 F# 环境, 取出的 Boy 就不再是 Boy 了 (ListOfBoyXXXXXXXX)

我并不是一个像 Q.yuhen 这样内功深厚的武林高手, 否则我会一直追问直到找出原因. 事实上我只是个样样好奇却样样平庸的功夫小子, 玩弄些花拳绣腿罢了, 所以尝试只能在此结束, 无法继续找出真正的答案了.