まえがき
最近までGoを書いていた私がC#に触れ始めました。今回は「アクセス修飾子」まわりについて理解したことを、Goとの比較を踏まえながら記事にしていこうと思います!
また比較といっていますが両言語は思想が異なるため、優劣をつける意図はありません。用途と前提が違うだけという視点でフラットに見ていきます。
C#の仕組みを理解しつつ、Goと比較しながら整理していきます。
アクセス修飾子まとめ(ざっくり比較)
まずはC#のアクセス修飾子を一覧で整理してみます。またGoと近似した機能も併せて記載します。
| C# のアクセス修飾子 | アクセス範囲 | Goの場合 | 備考 |
|---|---|---|---|
| public | 任意のアセンブリ(csproj) | 先頭を大文字で定義 例: Name string
|
外部公開用 |
| private | 同じクラス内 | 先頭を小文字で定義 例: secret string
|
Goはパッケージ内まで公開 |
| protected | 同じクラス内 or 派生クラス内 | なし | Goは継承がない |
| internal | 同じアセンブリ(csproj)内 |
internal/xxx ディレクトリ配下に配置 |
Goはディレクトリで制御 |
| protected internal | アセンブリ内 または 派生クラス内 | なし | - |
| private protected | アセンブリ内 かつ 派生クラス内 | なし | - |
見慣れない内容もあるかと思いますが、ここでは以下のようにアクセス制御するのだな、と思っていただければと思います。
- C# --> アクセス修飾子で制御
- Go --> 大文字or小文字始まりで構造体・フィールドを定義 + ディレクトリ構成で制御
この記事の前提
今回は、以下のディレクトリ構成で C# のアプリケーションを構築するという前提で説明を記載していきます。
// ドメイン層
Domain
└ User.Domain
└ User.Domain.csproj
// インフラ層
Infrastructure
└ User.Infrastructure
└ User.Infrastructure.csproj
// ユースケース層
UseCase
└ SampleWebApp001.UseCase
└ SampleWebApp001.UseCase.csproj
// ホスト(プレゼンテーション)層
Host
└ SampleWebApp001.Api
└ SampleWebApp001.Api.csproj
public
C#では、publicで定義すると任意のアセンブリ(csproj)のコードからアクセス可能になります。
Goでは、構造体名やメソッド名の先頭を大文字で定義すると任意のパッケージ(ディレクトリ)からアクセス可能になります。
C#
namespace User.Domain;
// publicなので、DomainUserクラスは外部からアクセス可能
public class DomainUser
{
// publicなので、Nameプロパティは外部からアクセス可能
public string Name { get; private set; }
private string _secretToken;
public DomainUser(string name, string token)
{
Name = name;
_secretToken = token;
}
// publicなので、SetNameメソッドは外部から呼び出し可能
public void SetName(string newName)
{
Name = newName;
}
private void Validate() { }
}
別のアセンブリ(csproj)でアクセスしようとすると
using User.Domain;
//【OK】publicなので、DomainUserの初期化可能
var user = new DomainUser("name","secret");
//【OK】publicなので、SetNameの呼び出し可能
user.SetName("hoge");
//【OK】publicなので、Nameの参照可能
Console.WriteLine($"Name: {user.Name}");
Go
package model
// DomainUser(先頭が大文字)のため、他パッケージ(ディレクトリ)からアクセス可能
type DomainUser struct {
// Name(先頭が大文字)のため、他パッケージ(ディレクトリ)からアクセス可能
Name string
secretToken string
}
// SetName(先頭が大文字)のため、他パッケージ(ディレクトリ)から呼び出し可能
func (u *DomainUser) SetName(newName string) {
// 他パッケージから呼び出せる
u.Name = newName
}
func (u *DomainUser) validate() {}
別のパッケージ(ディレクトリ)でアクセスしようとすると
package main // 別のパッケージ
import (
"go-sample-app/domain/model"
)
func main() {
//【OK】先頭が大文字なので、DomainUserの初期化可能
user := model.DomainUser{
//【OK】先頭が大文字なので、Nameにアクセス可能
Name: "name",
}
//【OK】先頭が大文字なので、SetNameの呼び出し可能
user.SetName("newName")
}
private
C#では、privateで定義すると同じクラスや構造体内のコードのみアクセス可能になります。
Goでは、構造体名やメソッド名の先頭を小文字で定義すると、同じパッケージ(ディレクトリ)内でのみアクセス可能になります。
C#
namespace User.Domain;
public class DomainUser
{
public string Name { get; private set; }
// privateなので、_secretTokenプロパティはこのクラス内のみアクセス可能
private string _secretToken;
public DomainUser(string name, string token)
{
Name = name;
_secretToken = token;
}
// privateなので、Validateメソッドはこのクラス内のみ呼び出し可能
private void Validate()
{
// クラス内部のみ利用
}
public static DomainUser NewUser(string name, string token)
{
var user = new DomainUser(name, token);
//【OK】同じクラス内なので、privateのメソッド呼び出し可能
user.Validate();
return user;
}
}
別のアセンブリでアクセスしようとすると
using User.Domain;
//【OK】クラスはpublicなので、DomainUserの初期化可能
var user = new DomainUser("name","secret");
//【NG】privateなので、_secretTokenを参照するとエラー
Console.WriteLine($"_secretToken: {user._secretToken}");
//【NG】privateなので、Validateを呼び出すとエラー
user.Validate();
Go
package model
type DomainUser struct {
Name string
// secretToken(先頭が小文字)のため、このパッケージ内でのみアクセス可能
secretToken string
}
// validate(先頭が小文字)のため、このパッケージ内でのみ呼び出し可能
func (u *DomainUser) validate() {
}
func NewUser() *DomainUser {
user := DomainUser{}
//【OK】同じパッケージ内なのでメソッド呼び出し可能
user.validate()
return &user
}
別のパッケージでアクセスしようとすると
package main // 別のパッケージ
import (
"go-sample-app/domain/model"
)
func main() {
//【OK】先頭が大文字なので、DomainUserの初期化可能
user := model.DomainUser{
Name: "name",
//【NG】先頭が小文字なので、secretTokenにアクセスするとエラー
secretToken: "",
}
//【NG】先頭が小文字なので、validateを呼び出すとエラー
user.validate()
}
補足
Goにはクラスのような概念がないので、C#のprivateとGoでは以下の違いがあります。
- C# のprivate --> クラス内のみアクセス可能
- Go の小文字 --> 同じパッケージ内のみアクセス可能
protected
C#では、protectedで定義すると同じクラスまたは派生クラスのコードのみアクセス可能になります。
Goでは、クラスや継承という概念がないため比較できる機能がありません。
C#
派生元のクラス
namespace User.Domain;
public class BaseUser
{
// protectedなので、派生クラスからのみアクセス可能
protected string Name;
// protectedなので、派生クラスからのみ呼び出し可能
protected void SetName(string name)
{
Name = name;
}
}
BaseUserからの派生クラス
namespace User.Domain;
public class AdminUser : BaseUser
{
public void SetAdminName()
{
// BaseUserの派生クラス内なので呼び出し可能
SetName("Admin");
}
};
Go
Goにはクラスや継承のような概念がないので、protected と比較できる機能はありません。
internal
C#では、internalで定義すると同じアセンブリ内のコードのみがアクセスできます。
Goでは、internalパッケージによってアクセスを制御することができます(※internalパッケージについては後で解説します)。
C#
namespace User.Infrastructure;
// internalなので、同じアセンブリのみアクセス可能
internal class UserRepository
{
public void Save() { }
};
同じアセンブリ内でアクセスしようとすると
namespace User.Infrastructure;
public class UserCommand
{
public static void Create()
{
// 【OK】同じアセンブリ内なので、初期化可能
var user = new UserRepository();
user.Save();
}
};
別のアセンブリ内でアクセスしようとすると
namespace SampleWebApp001.UseCase;
public class CreateUseCase
{
public async Task ExecuteAsync()
{
//【NG】internalのクラスなので、初期化するとエラー
var user = new UserRepository();
user.Save();
}
}
Go
C#のinternalと似た概念として、Goにはinternalパッケージという仕組みがあります。
internalパッケージは内部で使うパッケージを作成したいが、外部には参照させたくない場合に使用します。
この仕組みを使用することで、「特定ディレクトリ配下からのみ」 パッケージを参照することができます。
たとえば、以下のような構成の場合は、
project/apps/users/handler.goからはproject/apps/users/internal配下を参照できますが、
project/main.goからは参照できなくなります。
project
├─apps
│ └─users
│ ├─internal # usersパッケージのinternalパッケージ
│ │ ├─infrastructure
│ │ │ └─user_repository.go
│ │ └─usecase
│ │ └─user_usecase.go
│ └─handler.go #【OK】usersパッケージ配下なのでinternalパッケージにアクセス可能
├─domain
│ ├─model
│ └─repository
└─main.go #【NG】usersパッケージ配下でないのでアクセス不可
上記の構成からアクセス制御を行うと以下のようになります。
package infrastructure
// 「大文字 + internalパッケージ」--> internalパッケージと同じパッケージ配下のみアクセス可能
type UserRepository struct {}
// internalパッケージ内でのみ呼び出し可能
func (r *UserRepository) Save() {
}
// 「大文字 + internalパッケージ」--> internalパッケージと同じパッケージ配下のみ呼び出し可能
func NewUserRepository() repository.UserRepository {
return &userRepository{}
}
internalパッケージと同じパッケージ配下から実行すると
package users // internalパッケージと同じパッケージ
import (
"go-sample-app/apps/users/internal/infrastructure"
"go-sample-app/domain/repository"
)
type UserHandler struct {
repo repository.UserRepository
}
func NewUserHandler() *UserHandler {
//【OK】userパッケージ配下なので、infrastructure.NewUserRepositoryを呼び出し可能
userRepository := infrastructure.NewUserRepository()
return &UserHandler{
repo: userRepository,
}
}
外部のパッケージから実行すると
package main // internalパッケージと別のパッケージ
import (
//【NG】参照不可のためimport不可
"go-sample-app/apps/users/internal/infrastructure"
)
func main() {
//【NG】internalパッケージ内のメソッドは呼び出すと参照不可のためエラー
repo := infrastructure.NewUserRepository()
}
internalについて少しややこしいかなと思うのでまとめると
-
C# の internal -->
アセンブリ(csproj)を境界としたアクセスを制御。internalアクセスにしたクラスなどは外部のアセンブリからアクセス不可にする。 -
Go の internal -->
パッケージ(ディレクトリ)を境界としたアクセス制御。internalディレクトリ配下のコードをアクセス不可にする。
protected internal / private protected
C#では、protected internalで定義すると同じアセンブリ内のコード "または" 別のアセンブリ内の派生クラスのみがアクセスできます。
また、private protectedで定義すると同じアセンブリ内 "かつ" 同じクラスまたは派生クラスのみがアクセスできます。
Goでは、クラスや継承という概念がないため比較できる機能がありません。
C#
namespace User.Domain;
public class BaseUser
{
// 同じアセンブリ内 "または" 派生クラス
protected internal void ProtectedInternal () { }
// 同じアセンブリ内 "かつ" 継承
private protected void PrivateProtected () { }
}
同じアセンブリ内 "かつ" 派生クラスでアクセスしようとすると
public class LoginUser : BaseUser
{
public void MethodA()
{
//【OK】BaseUserの派生クラス内なのでアクセス可能
ProtectedInternal();
}
public void MethodB()
{
//【OK】同じアセンブリ内 "かつ" BaseUserの派生クラス内なのでアクセス可能
PrivateProtected();
}
};
別アセンブリ内 "かつ" 派生クラスでアクセスしようとすると
public class GeneralUser : BaseUser
{
public void MethodA()
{
//【OK】BaseUserの派生クラス内なのでアクセス可能
ProtectedInternal();
}
public void MethodB()
{
//【NG】BaseUserの派生クラス内だが別のアセンブリなのでアクセス不可
PrivateProtected();
}
};
Go
protectedと同様に、Goにはクラスや継承のような概念がないので、比較できる機能はありません。
closed
C#15 以降では新しく closed 修飾子が追加されます。本記事の作成時点ではまだリリースされていませんので簡単にご紹介します。
C#では、closedで定義すると同じアセンブリ内で直接の子孫になるクラスとその派生クラスからのみ派生することができるようになります。また closed クラスは暗黙的にabstractクラスとして扱われます。
Goでは、クラスや継承という概念がないため比較できる機能がありません。
// closedクラス
public closed record class JobStatus;
//【OK】同じアセンブリ内でclosedクラスの直接の子孫となるため派生可能
public record class Queued : JobStatus;
public record class Failed(string Error) : JobStatus;
別のアセンブリから派生しようとすると
//【NG】別のアセンブリなので派生不可
public record class Paused : JobStatus;
//【OK】別のアセンブリですが、closedクラスの派生クラスからの派生は可能
public record class RetryableFailed(string Error, int Attempts) : Failed(Error);
まとめ
-
C#はアクセス修飾子による細かいアクセス制御が可能
- public --> 外部公開
- private --> クラス内
- protected --> 派生先
- internal --> アセンブリ内
- etc...
-
Goはアクセス制御はシンプル
- 先頭が大文字 --> 公開
- 先頭が小文字 --> パッケージ内
- internal --> モジュール境界(ディレクトリルールで制御)
個人的な感想
C#のアクセス修飾子は、コードの安全性を高めるために強力な機能だと思いました。これはシステム開発においてレイヤーごとの責務をしっかり分けることができるので、アクセスレベルをしっかりと理解すれば非常に堅牢な設計が可能になると思いました。
一方で、GoはC#ほど厳密にアクセス制御は出来ませんが、「先頭が大文字・小文字」や「internalパッケージ」のようにコード定義やディレクトリ構造によってアクセス制御をする言語仕様はシンプルで迷いにくいかなと思います。
両言語の良さを理解しつつ今後の設計やシステム開発に活かしていきたいと思います!