web-dev-qa-db-fra.com

Comment limiter l'accès aux membres de classe imbriqués aux classes englobantes?

Est-il possible de spécifier que la classe englobante peut accéder aux membres d'une classe imbriquée, mais pas aux autres classes?

Voici une illustration du problème (bien sûr, mon code actuel est un peu plus complexe ...):

public class Journal
{
    public class JournalEntry
    {
        public JournalEntry(object value)
        {
            this.Timestamp = DateTime.Now;
            this.Value = value;
        }

        public DateTime Timestamp { get; private set; }
        public object Value { get; private set; }
    }

    // ...
}

J'aimerais empêcher le code client de créer des instances de JournalEntry, mais Journal doit pouvoir les créer. Si je rends le constructeur public, tout le monde peut créer des instances ... mais si je le rend privé, Journal ne le pourra pas!

Notez que la classe JournalEntry doit être publique, car je veux pouvoir exposer des entrées existantes au code client.

Toute suggestion serait appréciée !


UPDATE: Merci à tous pour votre contribution, je suis finalement allé à l'interface publique IJournalEntry, implémentée par une classe privée JournalEntry (malgré la dernière exigence de ma question ...)

53
Thomas Levesque

Si votre classe n'est pas trop complexe, vous pouvez utiliser une interface publique et rendre la classe d'implémentation privée ou créer un constructeur protégé pour la classe JornalEntry et disposer d'une classe privée JornalEntryInstance dérivée de JornalEntry avec un constructeur public qui est effectivement instancié par votre Journal.

public class Journal
{
    public class JournalEntry
    {
        protected JournalEntry(object value)
        {
            this.Timestamp = DateTime.Now;
            this.Value = value;
        }

        public DateTime Timestamp { get; private set; }
        public object Value { get; private set; }
    }

    private class JournalEntryInstance: JournalEntry
    { 
        public JournalEntryInstance(object value): base(value)
        { }
    }
    JournalEntry CreateEntry(object value)
    {
        return new JournalEntryInstance(value);
    }
}

Si votre classe actuelle est trop complexe pour faire l'une de ces choses et que vous pouvez vous en tirer, le constructeur n'étant pas complètement invisible, vous pouvez rendre le constructeur interne afin qu'il ne soit visible que dans Assembly.

Si cela aussi est infaisable, vous pouvez toujours rendre le constructeur privé et utiliser la réflexion pour l'appeler à partir de votre classe journal:

typeof(object).GetConstructor(new Type[] { }).Invoke(new Object[] { value });

Maintenant que j'y pense, une autre possibilité consisterait à utiliser un délégué privé dans la classe contenant qui est définie à partir de la classe interne.

public class Journal
{
    private static Func<object, JournalEntry> EntryFactory;
    public class JournalEntry
    {
        internal static void Initialize()
        {
            Journal.EntryFactory = CreateEntry;
        }
        private static JournalEntry CreateEntry(object value)
        {
            return new JournalEntry(value);
        }
        private JournalEntry(object value)
        {
            this.Timestamp = DateTime.Now;
            this.Value = value;
        }

        public DateTime Timestamp { get; private set; }
        public object Value { get; private set; }
    }

    static Journal()
    {
        JournalEntry.Initialize();
    }

    static JournalEntry CreateEntry(object value)
    {
        return EntryFactory(value);
    }
}

Cela devrait vous donner les niveaux de visibilité souhaités sans avoir à recourir à la réflexion lente ni à l'introduction de classes/interfaces supplémentaires

41
Grizzly

En réalité, il existe une solution simple et complète à ce problème qui ne nécessite pas de modifier le code client ni de créer une interface.

Cette solution est en réalité plus rapide que la solution basée sur une interface dans la plupart des cas et plus facile à coder.

public class Journal
{
  private static Func<object, JournalEntry> _newJournalEntry;

  public class JournalEntry
  {
    static JournalEntry()
    {
      _newJournalEntry = value => new JournalEntry(value);
    }
    private JournalEntry(object value)
    {
      ...
76
Ray Burns

Faites de JournalEntry un type imbriqué privé . Tous les membres publics ne seront visibles que par le type englobant.

public class Journal
{
    private class JournalEntry
    {
    }
}

Si vous devez rendre les objets JournalEntry disponibles à d'autres classes, exposez-les via une interface publique:

public interface IJournalEntry
{
}

public class Journal
{
    public IEnumerable<IJournalEntry> Entries
    {
        get { ... }
    }

    private class JournalEntry : IJournalEntry
    {
    }
}
17
Sam Harwell

Une approche plus simple consiste simplement à utiliser un constructeur internal, tout en obligeant l'appelant à prouver qui il est en fournissant une référence que seul l'appelant légitime pourrait savoir (nous n'avons pas à nous préoccuper de la réflexion non publique, car si l'appelant a accès à une réflexion non publique, la bataille est déjà perdue - ils peuvent accéder directement à un constructeur private); par exemple:

class Outer {
    // don't pass this reference outside of Outer
    private static readonly object token = new object();

    public sealed class Inner {
        // .ctor demands proof of who the caller is
        internal Inner(object token) {
            if (token != Outer.token) {
                throw new InvalidOperationException(
                    "Seriously, don't do that! Or I'll tell!");
            }
            // ...
        } 
    }

    // the outer-class is allowed to create instances...
    private static Inner Create() {
        return new Inner(token);
    }
}
10
Marc Gravell

Dans ce cas, vous pouvez soit:

  1. Rendre le constructeur interne - ceci empêche les personnes extérieures à cet assemblage de créer de nouvelles instances ou ... 
  2. Refactoriser la classe JournalEntry pour utiliser une interface publique et rendre la classe JournalEntry réelle privée ou interne. L'interface peut ensuite être exposée pour les collections pendant que l'implémentation réelle est masquée.

J'ai mentionné internal comme un modificateur valide ci-dessus, cependant, en fonction de vos besoins, privé peut être l'alternative la mieux adaptée. 

Edit: Désolé, j'ai mentionné le constructeur privé, mais vous avez déjà traité ce point dans votre question. Mes excuses pour ne pas le lire correctement!

3
Paul Mason

La solution suggérée par Grizzly rend difficile la création de la classe imbriquée ailleurs, mais pas impossible, comme Tim Pohlmann l’a écrit: quelqu'un peut toujours en hériter et utiliser la classe héritée Ctor.

Je profite du fait que la classe imbriquée peut accéder aux propriétés privées du conteneur, de sorte que le conteneur demande bien et que la classe imbriquée donne accès au ctor.

 public class AllowedToEmailFunc
{
    private static Func<long, EmailPermit> CreatePermit;

    public class EmailPermit
    {
        public static void AllowIssuingPermits()
        {
            IssuegPermit = (long userId) =>
            {
                return new EmailPermit(userId);
            };
        }

        public readonly long UserId;

        private EmailPermit(long userId) 
        {
            UserId = userId;
        }
    }

    static AllowedToEmailFunc()
    {
        EmailPermit.AllowIssuingPermits();
    }

    public static bool AllowedToEmail(UserAndConf user)
    {
        var canEmail = true; /// code checking if we can email the user
        if (canEmail)
        {
            return IssuegPermit(user.UserId);
        }
        else
        {
            return null
        }

    }
}

Cette solution n’est pas quelque chose que je ferais une journée de travail normale, non pas parce que cela poserait des problèmes à d’autres endroits, mais parce qu’elle est non conventionnelle (je ne l’ai jamais vue auparavant) et que cela pourrait causer des problèmes aux développeurs.

0
user1852503

Pour la classe générique imbriquée =)

Je sais que c’est une vieille question et que sa réponse est déjà acceptée. Néanmoins, pour les nageurs google qui peuvent avoir un scénario similaire, cette réponse peut apporter une aide.

Je suis tombé sur cette question car je devais mettre en œuvre la même fonctionnalité que le PO. Pour mon premier scénario ceci et ceci les réponses ont bien fonctionné. Néanmoins, j'avais également besoin d'exposer une classe générique imbriquée. Le problème est que vous ne pouvez pas exposer un champ de type délégué (le champ usine) avec des paramètres génériques ouverts sans rendre votre propre classe générique, mais ce n’est évidemment pas ce que nous souhaitons. Voici donc ma solution à ce scénario:

public class Foo
{
    private static readonly Dictionary<Type, dynamic> _factories = new Dictionary<Type, dynamic>();

    private static void AddFactory<T>(Func<Boo<T>> factory)
        => _factories[typeof(T)] = factory;

    public void TestMeDude<T>()
    {
        if (!_factories.TryGetValue(typeof(T), out var factory))
        {
            Console.WriteLine("Creating factory");
            RuntimeHelpers.RunClassConstructor(typeof(Boo<T>).TypeHandle);
            factory = _factories[typeof(T)];
        }
        else
        {
            Console.WriteLine("Factory previously created");
        }

        var boo = (Boo<T>)factory();
        boo.ToBeSure();
    }

    public class Boo<T>
    {
        static Boo() => AddFactory(() => new Boo<T>());

        private Boo() { }

        public void ToBeSure() => Console.WriteLine(typeof(T).Name);
    }
}

Nous avons Boo comme notre classe interne imbriquée avec un constructeur privé et nous maintenons sur notre classe mère un dictionnaire avec ces usines génériques tirant parti de dynamic . Ainsi, chaque fois que TestMeDude est appelé, Foo recherche si la fabrique pour T a déjà été créée, sinon elle le crée en appelant le constructeur statique de la classe imbriquée. 

Essai:

private static void Main()
{
    var foo = new Foo();

    foo.TestMeDude<string>();
    foo.TestMeDude<int>();
    foo.TestMeDude<Foo>();

    foo.TestMeDude<string>();

    Console.ReadLine();
}

La sortie est:

 enter image description here

0
taquion