淺談C#中的資料型別轉換與物件複製

曹化宇發表於2014-08-01

軟體開發中,資料型別的轉換是一項非常重要的工作,同時也是非常容易產生錯誤的地方;本文,我們就C#開發中的各種資料型別轉換做一些討論,供大家參考。這些內容包括:

  • C#語言內建轉換
  • as與is運算子
  • TryParse()方法
  • Convert類
  • 封裝轉換方法
  • 物件的傳遞、複製和序列化

C#語言內建轉換

在C#程式語言層面,內建了一些資料型別轉換的方法,如隱式轉換、強制(顯式)轉換,以及與物件相關的as和is運算子。下面分別進行討論。

隱式轉換。是指在表示式中有不同型別的資料進行運算,此時,就會將其中的一些資料的型別進行轉換,然後,表示式會使用相同型別的資料進行運算。如“float sum = 1.0f + 1;”表示式中,1.0f為float型別,而1則預設為int型別,此時,會自動將資料1轉換成float型別後再進行計算,而最終的結果也是float型別。為什麼是這樣?稍後討論。

強制轉換。在表示式中,我們可以在運算元前使用一對小括號指定目標型別,比如有一個float型別的變數fNum,在表示式“int sum = 2 + (int)fNum;”中,就可以將其轉換為int型別後進行計算。

隱式轉換和強制轉換操作一般用於數值型別的轉換。我們知道,不同資料型別的取值範圍是不同的;在進行隱式轉換時,會遵循一個基本的原則,即在轉換時,將取值範圍窄的型別轉換為取值範圍寬的型別,如果預設不能完成這樣的工作,編譯器就會報錯,如“int sum = 1.0f + 1;”語句就不能通過編譯,因為運算元1會隱式轉換為float,“1.0f + 1”的結果會是float,但是float不能隱式轉換為int型別(那樣就會丟掉數值的小數部分,而在資料的傳遞中,資料的丟失並不是一個好的選擇)。

如果我們在程式碼中需要明確的目標資料型別,就應該使用強制轉換,但在使用時必須要注意資料的丟失問題,特別是在浮點數或decimal型別轉換成整數型別的過程中。

此外,關於資料丟失,還有一些比較重要的概念,如取值範圍和溢位。表示式“int sum = int.MaxValue + 1;”中,sum的結果會是什麼呢?此語句在編譯時也會報錯,因為產生了溢位,資料型別不能儲存取值範圍(參考MinValue和MaxValue欄位)以外的資料。

當然,你也可以強制完成上面的計算,但必須使用unchecked關鍵字,如“int sum = unchecked(int.MaxValue + 1);”。但這樣一來,運算的結果恐怕比沒有結果更沒有意義。

as與is運算子

這是關於物件型別判斷和轉換的運算子。

使用is運算子,可以判斷一個物件是不是某個類的例項。比如,我們定義了一個arr物件為ArrayList型別,那麼“arr is ArrayList”的運算結果就是true。請注意下面的程式碼:

ArrayList arr = new ArrayList();
object obj = arr;
bool result = obj is ArrayList;

程式碼中,result的值會是什麼?答案是true,因為每個物件在傳遞時,其原始型別標識都會保留,所以,程式碼中依然可以判斷出物件的原始型別。

在使用is運算子判斷一個物件是否為一個型別後,我們可以使用as運算子將物件轉換為這個型別,然後,物件就可以呼叫此型別中定義的成員了。如在窗體中新增下面的程式碼:

foreach (object obj in this.Controls)
{
    if (obj is TextBox)
    {
        TextBox txt = obj as TextBox;
        txt.Text = txt.Name;
        txt.ForeColor = Color.Red;
    }
}

程式碼中,我們在遍歷窗體中的控制元件(使用obj物件),當這個控制元件是文字框控制元件(TextBox)時,使用as運算子轉換為TextBox型別物件txt,然後在文字框中顯示其名稱,並將字型設定為紅色;而直接使用object型別物件obj就無法完成這些工作。

TryParse()方法

在.NET Framework中定義的值型別中,都定義了TryParse()方法(.NET 2.0及以後版本)。此方法用於將字串轉換為相應的值型別,其語法為: bool TryParse(stringValue, out result);

其中,stringValue是需要轉換的字串內容,而result定義為輸出引數,其型別為相應的值型別,如int的TryParse()方法中的result引數定義為int型別。方法的返回值為bool型別,轉換成功返回true,否則返回false。如下面的程式碼:

int result;
if (int.TryParse("123", out result))
{
    Console.WriteLine("字串轉換結果為 {0}", result);
}
else
{
    Console.WriteLine("字串不能轉換為int型別");
}

Convert類

Convert類為靜態類,定義在System名稱空間。

正如其名,Convert類的功能就是提供了一系列的各種標準資料型別之間的轉換方法,這些方法名中都使用了.NET Framework型別中定義的資料型別名稱,如C#中的int就是Int32,將其它型別轉換為int型別的方法就是ToInt32()。如“int result = Convert.ToInt32("123");”。

使用Convert類時應注意,如果轉換不能正確完成,則會丟擲異常。

封裝轉換方法

前面,我們已經看到了在C#和.NET Framework類庫中提供的常用的資料型別轉換方法,我認為它有兩個問題:

  • 除了TryParse()方法,其它的轉換操作都有可能產生異常,這就需要在程式碼中很小心地進行處理。
  • TryParse()方法雖然不產生異常,但是,它只能對字串進行轉換,功能顯然不夠強大。

為了避免上述的兩個問題,我將標準值型別的轉換進行了封裝,每一種目標型別都有兩個方法,其中一個返回普通的值型別,另一個方法則返回相應的可空型別。以下是轉換為int型別的兩個方法:

public static int? ToIntNullable(object obj)
{
    if (obj == null) return null;
    int result;
    if (int.TryParse(obj.ToString(), out result))
        return result;
    else
        return null;
}

public static int ToInt(object obj)
{
    if (obj == null) return 0;
    int result;
    if (int.TryParse(obj.ToString(), out result))
        return result;
    else
        return 0;
}

先看一下ToIntNullable(object obj)方法,它的功能是將引數obj轉換成可空的int型別;當obj可以轉換成int型別時就返回轉換後結果,否則返回null值。為什麼對於int型別還要支援null值呢?這是為了與資料庫中的資料相容,我們知道,在資料庫中的資料欄位都有可能是null值的。通過可空型別,我們就可以完全相容的方式在C#程式碼和資料庫之間傳遞資料了。

而ToInt(object obj)方法,當obj可以轉換成int型別時返回轉換結果,否則返回0值。在C#程式碼中,如果只需要一個可以使用的int型別,而忽略空值或0的區別時,就可以使用這個方法,無論什麼情況下,都有一個int型別的結果可供使用。

程式碼中,還可以根據需要封裝其它型別的轉換方法。

在C#程式碼中使用這樣的轉換方法,可以有效提高編碼的效率。一方面可以有效避免可能的異常產生;另一方面,可以對空值和各種型別資料更方便地進行處理。在我的程式碼庫中,這些轉換方法會定義在一個名為CC的靜態類中。

物件的傳遞、複製和序列化

我們先來看一下物件傳遞的特點。如下面的程式碼:

ArrayList arr1 = new ArrayList();
arr1.Add("abc");
ArrayList arr2 = arr1;
Console.WriteLine("arr1[0]={0}, arr2[0]={1} ", arr1[0], arr2[0]);
arr2[0] = "123";
Console.WriteLine("arr1[0]={0}, arr2[0]={1} ", arr1[0], arr2[0]);

程式碼中,我們定義了兩個ArrayList物件arr1和arr2,它們實際上是指向了同一個“物件體”,這樣,我們修改其中的一個,實際上arr1和arr2指向的內容就同時改變了,此程式碼的顯示結果就是:

arr1[0]=abc, arr2[0]=abc
arr1[0]=123, arr2[0]=123

現在,我們看一個特殊的情況,那就是string型別的賦值。我們知道,string是引用型別,但是它的賦值卻和其它的引用型別不太一樣,這是因為,string儲存的是不可變字串,也就是說一個字串物件(真正的字串內容,而不是string物件變數)一旦建立,其內容是不能改變的,對於字串的任何修改,都會產生一個新的字串物件,如字串的連線、重新賦值等操作。如下面的程式碼:

string str1 = "abc";
string str2 = str1;
Console.WriteLine("str1={0}, str2={1}", str1, str2);
str2 = "123";
Console.WriteLine("str1={0}, str2={1}", str1, str2);

此程式碼的執行結果為:

str1=abc, str2=abc
str1=abc, str2=123

所以,當我們看到string物件與其它引用型別的物件在傳遞中會有不一樣的行為時,不應感到太多的驚訝。

回到我們的主題,在C#中,如果我們對物件進行賦值操作,就必須非常小心,如果修改了其中一個物件的內容,另一個物件的內容也會改變,這也是C#中預設的複製方式,我們稱為“淺複製”。淺複製在複製引用型別時,只會複製物件的引用,而不是物件內容真正的複製,如果我們需要完整地複製物件體,則需要使用“深複製”,其中的一個方法讓類支援ICloneable介面,實現其中的Clone()方法來完成物件的複製操作,實際操作上,這個方法實現起來還是比較複雜的。

此時,我們可以使用一個更加直觀的方法來完整複製物件,這個方法就是使用序列化。

一個型別如果要支援序列化,必須在類定義時使用SerializableAttribute特性,如:

[Serializable]
public class Class1
{
    // 類定義
}

在.NET Framework類庫中的很多型別都支援序列化操作,特別是資料容器或資料集合型別,ArrayList就是其中之一。

接下來,我們就要看看如何真正地完成序列化操作。序列化操作可以有兩個方向相反的操作,即:

  • 物件轉換為位元組陣列(序列化)。
  • 位元組陣列還原成物件(反序列化)。

為了方便使用,我們將這兩個操作分別封裝為兩個方法,如:

using System;
using System.Collections;
using System.IO;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters.Binary;

namespace chyx
{
    // 靜態類,封裝常用程式碼
    public static class CC
    {
        // 將物件序列化成位元組陣列
        public static byte[] ToBytes(object obj)
        {
            if (obj == null) return null;
            using (MemoryStream s = new MemoryStream())
            {
                IFormatter f = new BinaryFormatter();
                f.Serialize(s, obj);
                return s.GetBuffer();
            }
        }

        // 將位元組陣列反序列化成物件
        public static object ToObject(byte[] Bytes)
        {
            using (MemoryStream s = new MemoryStream(Bytes))
            {
                IFormatter f = new BinaryFormatter();
                return f.Deserialize(s);
            }
        }

        // 更多成員...
    }
}

我們如何利用序列化來複制物件呢?這時,我們可以在CC類定義一個Clone()方法來做這項工作,如:

public static object Clone(object obj)
{
    if (obj == null) return null;
    byte[] arrByte = ToBytes(obj);
    return ToObject(arrByte);
}

是不是很簡單,下面的程式碼,我們將對這個方法進行測試。

ArrayList arr1 = new ArrayList();
arr1.Add("abc");
ArrayList arr2 = CC.Clone(arr1) as ArrayList;
Console.WriteLine("arr1[0]={0}, arr2[0]={1} ", arr1[0], arr2[0]);
arr2[0] = "123";
Console.WriteLine("arr1[0]={0}, arr2[0]={1} ", arr1[0], arr2[0]);

由於我們使用Clone()完全複製了arr1,所以,arr2將是一個新的ArrayList物件,而不和arr2指向同一引用。程式碼的輸出結果如下:

arr1[0]=abc, arr2[0]=abc
arr1[0]=abc, arr2[0]=123

使用序列化和反序列化可以簡化複雜的物件的深複製(完全複製)操作,同時,我們還可以利用這一點對物件進行持久化操作,我們可以將一個物件序列化後儲存在一個檔案中(或其它形式),然後,可以讀取它並還原成物件。如下面的程式碼:

ArrayList arr1 = new ArrayList();
arr1.Add("abc");
byte[] bytes = CC.ToBytes(arr1);
string fileName=@"d:\arr1.object";
File.WriteAllBytes(fileName, bytes);
byte[] readBytes = File.ReadAllBytes(fileName);
ArrayList arr2 = CC.ToObject(readBytes) as ArrayList;
Console.WriteLine("arr1[0]={0}, arr2[0]={1} ", arr1[0], arr2[0]);

此程式碼的執行結果如下:

arr1[0]=abc, arr2[0]=abc

在介面、程式碼庫和資料庫三者之間,或者它們內部傳遞資料時,保證資料的完整性和正確性,以及保證傳遞的效率都是非常有挑戰性的工作;本文中所討論的內容是筆者在實際開發工作中一些關於資料傳遞和轉換方面的總結。有不當之處,還請各位批評指正,不勝感激!

相關文章