LoginSignup
5
2

More than 5 years have passed since last update.

C#, vb.netにおけるクラスの値渡し(簡易コピーと詳細コピー)

Posted at

はじめに

C#, vb.netにおける簡易コピーと詳細コピーについての記事です。
少し詰まってしまったので簡単なサンプルを使いながらまとめてみました。初心者の方も理解出来るようにわかりやすく書いたつもりです。
そのためコードのコメント量が少し多いかもしれません。
コードはC#, vb.netの両方を載せています。

注意点

筆者はvb.netを主に使用しているため、C#の知識はかなり浅いです。
コードの書き方に問題がありましたら指摘をお願いします。

前準備等

まずは基本となるクラス,メソッドを作成しました。

vb.netのコード

Class.vb
''' <summary>
''' 人物クラス
''' </summary>
Public Class Person
    Public name As String       ' 名前
    Public age As Integer       ' 年齢
    Public capacity As String() ' 所持資格
End Class

''' <summary>
''' 共通クラス
''' </summary>
Public Class Common
    ''' <summary>
    ''' データの有無チェック
    ''' </summary>
    ''' <param name="strData">文字列</param>
    ''' <returns>有→そのまま, 無→なし を返す</returns>
    Public Shared Function NullCheck(ByVal strData As String) As String
        Dim result As String = "なし"
        ' データが空でないならば引数をそのまま返す
        If Not String.IsNullOrEmpty(strData) Then
            result = strData
        End If
        Return result
    End Function
End Class
Program.vb
Public Class Program
    Private Sub Main()
        Dim personA As New Person()
        Dim personB As New Person()
        Dim personC As New Person()

        ' 人物データの設定
        personA.name = "山田太郎"
        personA.age = 40
        personA.capacity = {"基本情報", "応用情報"}

        personB.name = "田中花子"
        personB.age = 18
        personB.capacity = {}

        personC.name = "佐藤次郎"
        personC.age = 19
        personC.capacity = {"基本情報"}

        ' 人物データの表示
        ' string.joinは文字連結メソッド
        Console.Write("1人目 {0} 年齢:{1}歳 所持資格: {2} " & vbCrLf, personA.name, personA.age, Common.NullCheck(String.Join(", ", personA.capacity)))
        Console.Write("2人目 {0} 年齢:{1}歳 所持資格: {2} " & vbCrLf, personB.name, personB.age, Common.NullCheck(String.Join(", ", personB.capacity)))
        Console.Write("3人目 {0} 年齢:{1}歳 所持資格: {2} " & vbCrLf, personC.name, personC.age, Common.NullCheck(String.Join(", ", personC.capacity)))
    End Sub
End Class


ここからC#のコード

Class.cs
    /// <summary>
    /// 人物クラス
    /// </summary>
    class Person
    {
        public string name;  // 名前
        public int age;                 // 年齢
        public string[] capacity;           // 所持資格
    }

    /// <summary>
    /// 共通クラス
    /// </summary>
    class Common
    {
        /// <summary>
        /// データの有無チェック
        /// </summary>
        /// <param name="p">文字列</param>
        /// <returns>有→そのまま, 無→なし を返す</returns>
        public string NullCheck(string p)
        {
            string result = "なし";
            // データが空でないならば引数をそのまま返す
            if (!string.IsNullOrEmpty(p))
            {
                result = p;
            }
            return result;
        }
    }
Program.cs
static class Program
    {
        static void Main()
        {
            Person personA, personB, personC;

            personA = new Person();
            personB = new Person();
            personC = new Person();

            // 人物データの設定
            personA.name = "山田太郎";
            personA.age = 40;
            personA.capacity = new string[] { "基本情報", "応用情報" };

            personB.name = "田中花子";
            personB.age = 18;
            personB.capacity = new string[] { };

            personC.name = "佐藤次郎";
            personC.age = 19;
            personC.capacity = new string[] { "基本情報" };

            Common commonClass = new Common();

            // 人物データの表示
            // string.joinは文字連結メソッド
            Console.Write("1人目 {0} 年齢:{1}歳 所持資格: {2} \r\n", personA.name, personA.age, commonClass.NullCheck(string.Join(", ", personA.capacity)));
            Console.Write("2人目 {0} 年齢:{1}歳 所持資格: {2} \r\n", personB.name, personB.age, commonClass.NullCheck(string.Join(", ", personB.capacity)));
            Console.Write("3人目 {0} 年齢:{1}歳 所持資格: {2} \r\n", personC.name, personC.age, commonClass.NullCheck(string.Join(", ", personC.capacity)));
        }
    }

実行結果

実行結果
1人目 山田太郎 年齢:40歳 所持資格: 基本情報, 応用情報 
2人目 田中花子 年齢:18歳 所持資格: なし 
3人目 佐藤次郎 年齢:19歳 所持資格: 基本情報 

コードについて簡単に説明すると、

  • Personクラス→名前、年齢、所持資格のメンバーを持つ人物クラス。
  • Commonクラス→共通メソッドの作成。
  • Programクラス→Personクラスのインスタンスを作成後、データの設定を行い表示する。

特に難しい動作もなく実行結果も予想通りのものですね。

参照渡し

さてここに新しい人物を追加したいと思います。
「山田太郎」さんが20歳のときに生まれた息子「山田太郎Jr」さんです。
「山田太郎Jr」さんは「山田太郎」さんと共通点があるのでコピーを行ってから、データの編集を行いたいと思います。

Programクラスに以下のコードを追加しました。

Program.vb
Dim personD As Person

personD = personA  ' ★

' 人物データ編集
personD.name += "Jr"
personD.age -= 20
personD.capacity(1) = "ITパスポート"

' 表示
Console.Write("1人目 {0} 年齢:{1}歳 所持資格: {2} " & vbCrLf, personA.name, personA.age, Common.NullCheck(String.Join(", ", personA.capacity)))
Console.Write("4人目 {0} 年齢:{1}歳 所持資格: {2} " & vbCrLf, personD.name, personD.age, Common.NullCheck(String.Join(", ", personD.capacity)))
Program.cs
Person personD;

personD = personA;  // ★

// 人物データ編集
personD.name += "Jr";
personD.age -= 20;
personD.capacity[1] = "ITパスポート";

// 表示
Console.Write("1人目 {0} 年齢:{1}歳 所持資格: {2} \r\n", personA.name, personA.age, commonClass.NullCheck(string.Join(", ", personA.capacity)));
Console.Write("4人目 {0} 年齢:{1}歳 所持資格: {2} \r\n", personD.name, personD.age, commonClass.NullCheck(string.Join(", ", personD.capacity)));
実行結果
1人目 山田太郎Jr 年齢:20歳 所持資格: 基本情報, ITパスポート 
4人目 山田太郎Jr 年齢:20歳 所持資格: 基本情報, ITパスポート 

「山田太郎」さんのデータが「山田太郎Jr」さんのデータに置き換わってます。
クラスは参照型であるのため、値を変更すると元のデータの値も変更されるためです。

この部分の理解があやふやな方は以下の記事を参考にして下さい。
「値型と参照型の区別と違い、クラスと構造体の違い」

簡易コピー

それでは値渡しでクラスのデータを渡す方法を考えます。
少し調べてみると.netでは「Object.MemberwiseClone」といった「現在のObjectの簡易コピーを作成する」メソッドが用意されているようです。
さっそくこのメソッドをPersonクラスに作成し、実際に使用してみます。

Class.vb
Public Class Person
    Public name As String       ' 名前
    Public age As Integer       ' 年齢
    Public capacity As String() ' 所持資格

    ''' <summary>
    ''' 簡易コピー
    ''' </summary>
    ''' <returns>コピーデータ</returns>
    Public Function EasyClone() As Person
        Return CType(MemberwiseClone(), Person)
    End Function
End Class
Class.cs
class Person
    {
        public string name = string.Empty;  // 名前
        public int age = 0;                 // 年齢
        public string[] capacity;           // 所持資格

        /// <summary>
        /// 簡易コピーメソッド
        /// </summary>
        /// <returns>コピーデータ</returns>
        public Person EasyClone()
        {
            return (Person)MemberwiseClone();
        }
    }
先ほどのProgrmクラスの★の部分を変更する(C#, vb.net)
personD = personA 
⇒ personD = personA.EasyClone()
実行結果
1人目 山田太郎 年齢:40歳 所持資格: 基本情報, ITパスポート 
4人目 山田太郎Jr 年齢:20歳 所持資格: 基本情報, ITパスポート 

名前と年齢は無事値渡しを行えたようです。
しかし所持資格の部分は参照渡しのままのようです。
これはPersonのメンバーであるcapacityが配列(参照型)であるためです。簡易コピーでは参照先オブジェクトをコピーすることが出来ません。

詳細コピー

このままではcapacityを値渡しすることが出来ません。
そのためcapacityをさらに簡易コピーすることにします。capacityはString型の配列であるため「Array.Clone」メソッド(配列の簡易コピー。.Netで用意してあるメソッド)を使用して簡易コピーします。
これを利用することですべてのデータを値渡しすることが出来ます。
Personクラスに以下のようにDeepCloneメソッドを作成しました。

Class.vb
Public Class Person
'~~~~~~~~~~~~~~~~省略
    ''' <summary>
    ''' 詳細コピー
    ''' </summary>
    ''' <returns>コピーデータ</returns>
    Public Function DeepClone() As Person
        ' 簡易コピー(Personクラス)を作成
        Dim clone As Person = CType(MemberwiseClone(), Person)
        ' 参照型のcapacityを簡易コピー(capacity)して置き換える
        If MyClass.capacity IsNot Nothing Then
            clone.capacity = MyClass.capacity.Clone()
        End If
        ' 詳細コピーデータを返す
        Return clone
    End Function
End Class
Class.cs
class Person
    {
//~~~~~~~~~~~~~~~~省略
        /// <summary>
        /// 詳細コピーメソッド
        /// </summary>
        /// <returns>コピーデータ</returns>
        public Person DeepClone()
        {
            // 簡易コピー
            Person cloned = (Person)MemberwiseClone();

            // 参照型フィールドの複製を作成する(簡易コピーを行う)
            if (this.capacity != null)
            {
                cloned.capacity = (string[])this.capacity.Clone();
            }
            return cloned;
        }
    }
Progrmクラスの★の部分を変更する(C#, vb.net)
personD = personA 
⇒ personD = personA.DeepClone()
実行結果
1人目 山田太郎 年齢:40歳 所持資格: 基本情報, 応用情報
4人目 山田太郎Jr 年齢:20歳 所持資格: 基本情報, ITパスポート 

参考にさせていただいたサイト。「オブジェクトの複製」
この記事では説明していない、コピーコンストラクタ、シリアライズによる複製の説明もありますのでより詳しく学習したい方は一度目を通すことをおすすめします。

もう少し詳しく

ここまででしたら上記のサイトを参考にしたりしてスムーズに行えましたが、次のようなクラスの詳細コピーを行う際に少し詰まってしまいました。

[会社クラス]
{
会社名(String型)
社長(Personクラス)
従業員(Personクラスの配列)
}

クラスの中に別のクラス、または別のクラスの配列が存在する場合です。
こちらの詳細コピーは以下のようにして行いました。

Class.vb

''' <summary>
''' 会社クラス
''' </summary>
Public Class Company
    Public name As String       ' 会社名
    Public ceo As Person        ' 社長
    Public employee As Person() ' 従業員

    ''' <summary>
    ''' 詳細コピー
    ''' </summary>
    ''' <returns>コピーデータ</returns>
    Public Function DeepClone() As Company    
        ' 簡易コピー(Personクラス)を作成
        ' この時点ではnameメンバーのみが値渡し
        Dim clone As Company = CType(MemberwiseClone(), Company)

        If Not MyClass.ceo Is Nothing Then
            ' 先ほど作成したPersonクラスの詳細コピーメソッドを呼び出して詳細コピー
            ' ceoメンバーも値渡しすることが出来た
            clone.ceo = CType(MyClass.ceo.DeepClone(), Person)
        End If

        ' 従業員のリストを作成(Personクラスのリスト)
        Dim employeeList As New List(Of Person)

        If MyClass.employee IsNot Nothing Then
            ' Personクラスのデータをすべて取得
            For Each individual As Person In MyClass.employee               
                If individual IsNot Nothing Then
                    ' 詳細コピーメソッドを呼び出し詳細コピーしたものをリストに追加する
                    employeeList.Add(CType(individual.DeepClone(), Person))
                Else
                    ' データがない場合は空データをリストに追加                    
                    employeeList.Add(Nothing)
                End If
            Next
            clone.employee = employeeList.ToArray()
        End If

        Return clone
    End Function

End Class

Program.vb

Public Class Program
    Private Sub Program

        ' ~~~~~~省略

        Dim companyA As New Company()       ' 会社データA(旧)

        ' 会社情報設定
        companyA.name = "株式会社A"
        companyA.ceo = personA
        companyA.employee = {personB, personC, personD}

        Dim nameList As String()            ' 従業員の名前リスト
        Dim tempList As New List(Of String) ' データ作成用リスト

        ' 従業員の名前をすべて取得しデータ作成用リストに追加する
        For Each employee As Person In companyA.employee
            tempList.Add(employee.name)
        Next

        ' 従業員の名前リスト作成
        nameList = tempList.ToArray()

        ' 出力
        Console.Write("会社名 {0}(旧) 社長:{1} 社員: {2} " & vbCrLf, companyA.name, companyA.ceo.name, Common.NullCheck(String.Join(", ", nameList)))

        Dim newCompanyA As Company = companyA.DeepClone()       ' 会社データA(新)[会社データA(旧)を詳細コピー]

        ' 社長→山田太郎Jrに変更、社員から山田太郎Jrを削除
        newCompanyA.ceo = personD
        newCompanyA.employee(2) = Nothing


        Dim nameList2 As String()
        tempList = New List(Of String)

        ' 従業員の名前をすべて取得しデータ作成用リストに追加する
        For Each employee As Person In newCompanyA.employee
            If employee IsNot Nothing Then
                tempList.Add(employee.name)
            Else
                ' 今回は空データはスキップするが、必要に応じてListに空データを追加する
                'tempList.Add(Nothing)
            End If
        Next

        nameList2 = tempList.ToArray()

        ' 出力
        Console.Write("会社名 {0}(新) 社長:{1} 社員: {2} " & vbCrLf, newCompanyA.name, newCompanyA.ceo.name, Common.NullCheck(String.Join(", ", nameList2)))

    End Sub
End Class

ここからC#

Class.cs

    class Company
    {
        public string name = string.Empty;
        public Person ceo;
        public Person[] employee;

        /// <summary>
        /// 詳細コピーメソッド
        /// </summary>
        /// <returns>コピーデータ</returns>
        public Company DeepClone()
        {
            // 簡易コピー(Personクラス)を作成
            // この時点ではnameメンバーのみが値渡し
            Company cloned = (Company)MemberwiseClone();

            // 先ほど作成したPersonクラスの詳細コピーメソッドを呼び出して詳細コピー
            // ceoメンバーも値渡しすることが出来た
            if (this.ceo != null)
            {
                cloned.ceo = ceo.DeepClone();
            }

            if (this.employee != null)
            {
                // 従業員のリストを作成(Personクラスのリスト)
                List<Person> employeeList  = new List<Person>();

                foreach (Person individual in this.employee)
                {
                    if (individual != null)
                    {
                        // 詳細コピーメソッドを呼び出し詳細コピーしたものをリストに追加する
                        employeeList .Add(individual.DeepClone());
                    }
                    else
                    {
                        // データがない場合は空データをリストに追加  
                        employeeList .Add(null);
                    }
                }
                cloned.employee = employeeList .ToArray();
            }
            return cloned;
        }
    }
Program.cs

static class Program
{

    static void Main()
    {
        // ~~~~~~省略
        Company companyA = new Company();

        // 会社情報設定
        companyA.name = "株式会社A";        // ' 会社データA(旧)
        companyA.ceo = personA;
        companyA.employee = new Person[] { personB, personC, personD };

        string[] nameList;                          // 従業員の名前リスト
        List<string> tempList = new List<string>(); // データ作成用リスト

        // 従業員の名前をすべて取得しデータ作成用リストに追加する
        foreach (Person employee in companyA.employee)
        {              
            tempList.Add(employee.name);
        }

        // 従業員の名前リスト作成
        nameList = tempList.ToArray();

        // 出力
        Console.Write("会社名 {0}(旧) 社長:{1} 社員: {2} \r\n", companyA.name, companyA.ceo.name, commonClass.NullCheck(string.Join(", ", nameList)));

        Company newCompanyA = companyA.DeepClone();     // 会社データ(新)A[会社データ(旧)Aを詳細コピー]

        // 社長→山田太郎Jrに変更、社員から山田太郎Jrを削除
        newCompanyA.ceo = personD;
        newCompanyA.employee[2] = null;


        string[] nameList2;
        tempList = new List<string>();

        // 従業員の名前をすべて取得しデータ作成用リストに追加する
        foreach (Person employee in newCompanyA.employee)
        {
            if (employee != null)
            {
                tempList.Add(employee.name);
            }
            // 今回は空データはスキップするが、必要に応じてListに空データを追加する
            //else
            //{
            //    tempList.Add(null);
            //}
        }

        nameList2 = tempList.ToArray();

        // 出力
        Console.Write("会社名 {0}(新) 社長:{1} 社員: {2} \r\n", newCompanyA.name, newCompanyA.ceo.name, commonClass.NullCheck(string.Join(", ", nameList2)));
    }
}

実行結果
会社名 株式会社A(旧) 社長:山田太郎 社員: 田中花子, 佐藤次郎, 山田太郎Jr 
会社名 株式会社A(新) 社長:山田太郎Jr 社員: 田中花子, 佐藤次郎

おわりに

サンプルを使ったほうがわかりやすいかと思いましたが、かなり長くなってしまいました……。
またコメントも適切ではないかもしれませんが、参考にしていただければ幸いです。

5
2
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
2