Knowing and understanding the four visibility modifiers of Solidity is a base requirement for any Solidity developer.
Interestingly, those visibility modifiers have some pretty different or additional semantics from those of other languages you might already be comfortable with.
What Visibility Modifiers Are
Visibility modifiers define the visibility of state variables or functions.
Solidity is a contract-oriented language. Like in Java, where everything is contained within an object, all code in Solidity is contained within a contract (or a library). Semantically, contracts and libraries are nothing else than objects.
Whether you can access a property or a function of an object (or contract or library) from the outside is defined by its visibility. Only state variables and functions declared as "visible from the outside" can be accessed outside of that contract, for example.
Visibility Modifiers In Solidity
Solidity knows 4 visibility modifiers, each with its own semantics. These are:
- external
- public
- internal
- private
We will go over each of them in more detail now.
external
Only functions can be marked external. External functions are part of the contract interface and can be called from other contracts and transactions. They can't be called internally. This leads to external functions being cheaper to execute.
Arguments of external functions can be directly read from the calldata. They don't need to be copied over to memory like for public functions. The reason is that internal calls are executed through jumps in the code (under the hood). Array arguments are internally passed as memory pointers. Because of this, internal calls always need all variables in memory.
Given this:
contract MyContract {
function someExternalFunction() external view returns (uint8) {
return 0;
}
}
This works:
contract OtherContract {
function myFunc() public view returns (uint8) {
MyContract c = new MyContract();
c.someExternalFunction();
}
}
But this doesn't:
contract OtherContract {
function myFunc() external view returns (uint8) {
// ...
}
function myOtherFunc() external view returns (uint8) {
myFunc(); // Nope, doesn't work!
}
}
Best practice: Always use external when you don't want to call the function from within the same contract. This saves gas.
public
Both state variables (the 'properties' of your contract) and functions can be marked as public.
Public state variables and functions can both be accessed from the outside and the inside. The Solidity compiler automatically creates a getter function for them.
For a public state variable myVar
, the compiler generates an automatic getter function myVar() that returns the value of myVar
. When myVar()
is called from within the same contract, myVar
is actually accessed. If accessed externally, the function is evaluated.
This is what happens when you call an auto-generated getter from the same contract:
contract MyContract {
uint256 public myVar;
function myFunc() public view returns (uint256) {
// Automatically skips the function and accesses myVar
uint256 myVarCopy = myVar();
}
}
And this is what happens when you call it from the outside.
contract OtherContract {
function myFunc() public view returns (uint256) {
MyContract c = new MyContract();
// Function access is used here
c.myVar();
}
}
Best practice: Use public for publicly accessible state variables and when you want functions accessible from the outside and inside.
internal
internal is the default visibility for state variables.
Internal functions and state variables can both be accessed from within the same contract and in deriving contracts. They aren't accessible from the outside.
This works:
contract MyContract {
uint256 internal myVar;
function myFunc() public view returns (uint256) {
uint256 myVarCopy = myVar;
}
}
This also works:
contract DerivedContract is MyContract {
function myFunc() public view returns (uint256) {
uint256 myVarCopy = myVar;
}
}
But this doesn't work:
contract OtherContract {
function myFunc() public view returns (uint256) {
MyContract c = new MyContract();
// Nope this does not work. myVar is private to MyContract.
c.myVar;
}
}
Best practice: Use internal whenever you think state variables and functions should also be accessible in deriving contracts.
private
Private is the most restrictive visibility.
State variables and functions marked as private are only visible and accessible in the same contract.
Important: Private is only a code-level visibility modifier. Your contract state marked as private is still visible to observers of the blockchain. It is just not accessible for other contracts.
This works:
contract MyContract {
uint256 private myVar;
function myFunc() public view returns (uint256) {
// This works, it's the same contract
uint256 myVarCopy = myVar;
}
}
But this doesn't:
contract OtherContract {
function myFunc() public view returns (uint256) {
MyContract c = new MyContract();
// Nope this does not work. myVar is private to MyContract.
c.myVar;
}
}
And this does neither work:
contract DerivedContract is MyContract {
function myFunc() public view returns (uint256) {
// Nope, this doesn't work.
// Although this contract is derived from MyContract,
// myVar is local to MyContract and not visible here.
uint256 myVarCopy = myVar;
}
}
Best practice: Use private when you really want to protect your state variables and functions because you hide them behind logic executed through internal or public functions.
Conclusion
Solidity's visibility modifiers are a little different from those of other languages, but they still work mostly like those you know already.
It should not take you too much time to learn them all, and you definitely should work on learning them.
Especially external functions can make a huge difference in gas consumption.
Before You Leave
If you would love to read even more content like this, feel free to visit me on Twitter or LinkedIn.
I'd love to count you as my ever-growing group of awesome friends!