通过 WCF 认识 F# 中的类型

标签 WCF Fsharp

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

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

#!fsharp
(* 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
}

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

#!fsharp
// 给 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 工具生成客户端代码

#!csharp
// 以下是客户端服务引用自动生成的代码片段
[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 之类的呢? 试一下便清楚了!

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

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

#!csharp
// 生成的 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 这样内功深厚的武林高手, 否则我会一直追问直到找出原因. 事实上我只是个样样好奇却样样平庸的功夫小子, 玩弄些花拳绣腿罢了, 所以尝试只能在此结束, 无法继续找出真正的答案了.